=Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start
40 files modified
96 files added
| | |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{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%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.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:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px 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>.toggle{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>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}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-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.always.fixed>ul{padding-top:var(--btn)}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@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))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.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}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);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{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height: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)}.wp-site-blocks>header,body>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)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none} |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul,ul.socials{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%;padding:0;margin:0}nav.col,nav.col ul{height:max-content}nav>ul{width:100%;overflow:auto hidden}nav li{width:max-content;--justify:center;max-inline-size:none;padding:0;list-style:none}nav.fill li{width:100%}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.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:1px solid var(--action-0);outline-offset:1px}nav.always{overflow:visible;transition:width var(--trans-base);width:max-content}nav.always>ul{--dir:column;--align:center;--justify:flex-end;--gap:0;height:100vh;max-height:none;position:fixed;right:-300vw;bottom:0;width:100vw;padding:var(--btn) 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{width:100%}nav.always.fixed{width:var(--btn);height:var(--btn);bottom:0;right:0;overflow:hidden}nav.always.fixed .toggle.main{background-color:var(--base)}nav.always.fixed .toggle.main:focus,nav.always.fixed .toggle.main:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.mobile .toggle.main{width:var(--btn);transition:width var(--trans-base)}nav.mobile .icon-list,nav.mobile .icon-x{--w:32px}nav.mobile .icon-x,nav.mobile.open .icon-list{display:none}nav.mobile .icon-list,nav.mobile.open .icon-x{display:block}nav.mobile.open>ul{--dir:column;z-index:var(--z-9);background-color:rgba(var(--base-rgb),var(--op-6));width:100vw;height:100vh;overflow:hidden auto;right:0;bottom:0;position:fixed;padding:var(--btn) 0}nav.always>ul::before,nav.mobile.open>ul::before{content:'';z-index:-1;position:absolute;inset:0;filter:blur(5px)}nav.always.open .main.toggle,nav.mobile.open .main.toggle{position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-10);aspect-ratio:unset}nav.always>ul,nav.always>ul:before,nav.mobile.open>ul,nav.mobile.open>ul::before{background-color:rgba(var(--base-rgb),var(--op-6))}@media (max-width:767px){nav.col{height:var(--btn)}nav.mobile>ul{--dir:column;--align:center;--justify:flex-end;--gap:0;height:100%;max-height:none;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto}nav.mobile.open>ul{right:0}}@media (min-width:768px){nav.mobile:not(.always) .toggle.main{display:none}}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-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{left:0;bottom:0;width:calc(100% - var(--btn))}nav.fixed:not(.always) ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed:not(.always) li{flex:1}nav.fixed a{--align:center;--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@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))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.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}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}.condensed ul{--justify:center;--dir:row}nav.condensed{--gap:0 .25rem;width:100%;--justify:center}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:flex-end;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x;width:100%}ul.socials li{list-style:none}.always ul.socials{width:100vw;--justify:stretch}.always ul.socials li{flex:1;--justify:center;--align:center}.always ul.socials a{display:inline-flex}ul.socials a{display:inline-block;font-size:var(--txt-x-small);padding:.25rem .5rem;max-width:none}ul.socials .icon{margin:0}ul.socials .icon+span:not(.screen-reader-text){margin-left:.5rem}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height: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)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:var(--align,center);justify-content:var(--justify,space-between);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)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}nav.pagination{width:100%}nav.pagination>a{min-width:var(--chipchip)}nav.pagination>ul{margin:0 auto}nav.pagination:not(:has(a+ul)){margin-left:var(--chipchip)}nav.pagination:not(:has(ul+a)){margin-right:var(--chipchip)}.pagination.condensed li+li::before{display:none}.pagination li.current{width:var(--chip_);height:var(--chip_);background-color:var(--action-0);border-radius:var(--radius);line-height:1}.pagination.condensed a{font-size:var(--txt-medium);width:var(--chip_);height:var(--chip_)} |
| | |
| | | { passive: true } |
| | | ); |
| | | |
| | | |
| | | window.previousBGSize = 'Small'; |
| | | window.bgSizes = { |
| | | Small: 500, |
| | | Med: 768, |
| | | Large: 1024 |
| | | }; |
| | | |
| | | window.bgObserver = new IntersectionObserver((entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | let newSize = entry.target.dataset[`bg${window.previousBGSize}`]; |
| | | entry.target.style.backgroundImage = `url(${newSize})`; |
| | | entry.target.dataset.bgImg = window.previousBGSize; |
| | | window.bgObserver.unobserve(entry.target); |
| | | } |
| | | }) |
| | | }, |
| | | { |
| | | root: null, |
| | | rootMargin: '0px 0px -100px 0px', |
| | | threshold: 0 |
| | | }); |
| | | |
| | | function updateBG() { |
| | | let current = window.innerWidth; |
| | | let newWidth = getBGWidth(current); |
| | | |
| | | if (newWidth) { |
| | | window.previousBGSize = newWidth; |
| | | document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(img => { |
| | | window.bgObserver.observe(img); |
| | | }); |
| | | } |
| | | |
| | | } |
| | | function getBGWidth(width) { |
| | | let prev = window.previousBGSize; |
| | | |
| | | let check = { |
| | | Small: ['Med','Large'], |
| | | Med: ['Large'], |
| | | Large: false |
| | | }; |
| | | |
| | | if (!check[prev]) { |
| | | return false; |
| | | } |
| | | let next = 'Small'; |
| | | |
| | | check[prev].forEach(w => { |
| | | if (width => window.bgSizes[w]) { |
| | | next = w; |
| | | } |
| | | }); |
| | | return next; |
| | | } |
| | | |
| | | updateBG(); |
| | | |
| | | // Debounced resize to recalc scrollable height |
| | | window.addEventListener('resize', () => { |
| | | window.debouncer.schedule('recalc-max-scroll', () => { |
| | | updateMaxScroll(); |
| | | updateScrollProgress(window.scrollY || docEl.scrollTop || 0); |
| | | }, 20); |
| | | window.debouncer.schedule('bg-resize', () => { |
| | | updateBG(); |
| | | }); |
| | | }); |
| | | |
| | | // Initial setup |
| | |
| | | this.navs = new Map(); |
| | | document.querySelectorAll('nav:has(.submenu), nav:has(.toggle)').forEach(nav => { |
| | | let navID = nav.id; |
| | | if (navID === '') { |
| | | if (navID === '' || this.navs.has(navID)) { |
| | | navID = `nav-${this.counter}`; |
| | | nav.id = navID; |
| | | this.counter++; |
| | |
| | | (()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.openSubmenu=null,this.releaseFocusTrap=null,this.clicked=new Set,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.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)));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.counter++,n.forEach(e=>{e.id="submenu-"+this.counter,e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)),this.counter++}),this.navs.set(t,a)})}initListeners(){this.clickListener=this.handleClick.bind(this),this.keysListener=this.handleKeys.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;let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav"),s=!this.clicked.has(e);return e.classList.contains("open")!==s&&this.toggleNav(s,e.id),void(s?this.clicked.add(e):this.clicked.delete(e))}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li"),t=!this.clicked.has(e),n=e.classList.contains("open")!==t;return t?this.clicked.add(e):this.clicked.delete(e),void(n&&this.toggleSubmenu(t,e))}if(!this.openNav)return;let n=!0;for(let[t,s]of this.navs)if(e.target.closest("#"+t)){n=!1;break}n&&this.toggleNav(!1,this.openNav)}handleHoverOn(e){let t=e.currentTarget;this.clicked.has(t)||t.closest("nav.sidebar")||(t.classList.contains("has-submenu")?this.toggleSubmenu(!0,t):"NAV"===t.tagName&&(t.classList.contains("mobile")||this.toggleNav(!0,t.id)))}handleHoverOff(e){let t=e.currentTarget;if(!this.clicked.has(t)&&!t.closest("nav.sidebar"))if(t.classList.contains("has-submenu"))this.toggleSubmenu(!1,t);else if("NAV"===t.tagName){if(t.classList.contains("mobile"))return;let e=this.navs.get(t.id),s=!0;for(let t of e.submenus)if(this.clicked.has(t)){s=!1;break}s&&this.toggleNav(!1,t.id)}}handleKeys(e){if(this.openNav)switch(e.key){case"Escape":this.closeAll();break;case"ArrowDown":this.focusNextItem(),e.preventDefault();break;case"ArrowUp":this.focusPrevItem(),e.preventDefault()}}closeAll(){let e=this.navs.get(this.openNav);e&&this.clicked.has(e.nav)&&this.clicked.delete(e.nav),this.toggleNav(!1,this.openNav)}focusNextItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t+1]||e[0]).focus()}focusPrevItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t-1]||e[e.length-1]).focus()}getFocusableItems(){return Array.from(document.querySelectorAll("nav.open a, nav.open button")).filter(e=>!e.disabled&&!e.closest("[hidden]")&&!e.closest("[inert]"))}toggleNav(e,t){let s=this.navs.get(t);if(s){if(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.keysListener),s.nav.classList.contains("mobile")&&(this.releaseFocusTrap=window.jvbA11y.trapFocus(s.nav))):(this.releaseFocusTrap&&(this.releaseFocusTrap(),this.releaseFocusTrap=null),this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.keysListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach(e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}),Array.from(s.submenus).forEach(e=>{this.clicked.has(e)&&this.clicked.delete(e)})),s.nav.classList.toggle("open",e),s.nav.classList.contains("mobile")){const t=s.nav.querySelector(":scope > ul");t&&(t.inert=!e)}s.nav.setAttribute("aria-expanded",e),e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus()}}toggleSubmenu(e,t){e&&this.openSubmenu&&this.openSubmenu!==t&&!this.openSubmenu.contains(t)&&this.toggleSubmenu(!1,this.openSubmenu),e?this.openSubmenu=t:this.openSubmenu===t&&(this.openSubmenu=null,this.clicked.delete(t));let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];e||s.focus();const i=t.querySelector(":scope > ul");i&&(i.inert=!e);let a=s.getAttribute("aria-label");window.jvbA11y.announce(e?`${a} expanded`:`${a} collapsed`),t.classList.toggle("open",e),s.setAttribute("aria-expanded",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.openSubmenu=null,this.releaseFocusTrap=null,this.clicked=new Set,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach(e=>{let t=e.id;(""===t||this.navs.has(t))&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)));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.counter++,n.forEach(e=>{e.id="submenu-"+this.counter,e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)),this.counter++}),this.navs.set(t,a)})}initListeners(){this.clickListener=this.handleClick.bind(this),this.keysListener=this.handleKeys.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;let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav"),s=!this.clicked.has(e);return e.classList.contains("open")!==s&&this.toggleNav(s,e.id),void(s?this.clicked.add(e):this.clicked.delete(e))}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li"),t=!this.clicked.has(e),n=e.classList.contains("open")!==t;return t?this.clicked.add(e):this.clicked.delete(e),void(n&&this.toggleSubmenu(t,e))}if(!this.openNav)return;let n=!0;for(let[t,s]of this.navs)if(e.target.closest("#"+t)){n=!1;break}n&&this.toggleNav(!1,this.openNav)}handleHoverOn(e){let t=e.currentTarget;this.clicked.has(t)||t.closest("nav.sidebar")||(t.classList.contains("has-submenu")?this.toggleSubmenu(!0,t):"NAV"===t.tagName&&(t.classList.contains("mobile")||this.toggleNav(!0,t.id)))}handleHoverOff(e){let t=e.currentTarget;if(!this.clicked.has(t)&&!t.closest("nav.sidebar"))if(t.classList.contains("has-submenu"))this.toggleSubmenu(!1,t);else if("NAV"===t.tagName){if(t.classList.contains("mobile"))return;let e=this.navs.get(t.id),s=!0;for(let t of e.submenus)if(this.clicked.has(t)){s=!1;break}s&&this.toggleNav(!1,t.id)}}handleKeys(e){if(this.openNav)switch(e.key){case"Escape":this.closeAll();break;case"ArrowDown":this.focusNextItem(),e.preventDefault();break;case"ArrowUp":this.focusPrevItem(),e.preventDefault()}}closeAll(){let e=this.navs.get(this.openNav);e&&this.clicked.has(e.nav)&&this.clicked.delete(e.nav),this.toggleNav(!1,this.openNav)}focusNextItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t+1]||e[0]).focus()}focusPrevItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t-1]||e[e.length-1]).focus()}getFocusableItems(){return Array.from(document.querySelectorAll("nav.open a, nav.open button")).filter(e=>!e.disabled&&!e.closest("[hidden]")&&!e.closest("[inert]"))}toggleNav(e,t){let s=this.navs.get(t);if(s){if(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.keysListener),s.nav.classList.contains("mobile")&&(this.releaseFocusTrap=window.jvbA11y.trapFocus(s.nav))):(this.releaseFocusTrap&&(this.releaseFocusTrap(),this.releaseFocusTrap=null),this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.keysListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach(e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}),Array.from(s.submenus).forEach(e=>{this.clicked.has(e)&&this.clicked.delete(e)})),s.nav.classList.toggle("open",e),s.nav.classList.contains("mobile")){const t=s.nav.querySelector(":scope > ul");t&&(t.inert=!e)}s.nav.setAttribute("aria-expanded",e),e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus()}}toggleSubmenu(e,t){e&&this.openSubmenu&&this.openSubmenu!==t&&!this.openSubmenu.contains(t)&&this.toggleSubmenu(!1,this.openSubmenu),e?this.openSubmenu=t:this.openSubmenu===t&&(this.openSubmenu=null,this.clicked.delete(t));let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];e||s.focus();const i=t.querySelector(":scope > ul");i&&(i.inert=!e);let a=s.getAttribute("aria-label");window.jvbA11y.announce(e?`${a} expanded`:`${a} collapsed`),t.classList.toggle("open",e),s.setAttribute("aria-expanded",e),e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",function(){window.jvbNav=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.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},window.focusNextElement=function(){if(document.activeElement&&document.activeElement.form){var e=Array.prototype.filter.call(document.activeElement.form.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'),function(e){return e.offsetWidth>0||e.offsetHeight>0||e===document.activeElement}),t=e.indexOf(document.activeElement);if(t>-1)(e[t+1]||e[0]).focus()}}})(); |
| | | (()=>{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}function c(){window.innerWidth;let e=function(){let e=window.previousBGSize,t={Small:["Med","Large"],Med:["Large"],Large:!1};if(!t[e])return!1;let n="Small";return t[e].forEach(e=>{(t=>window.bgSizes[e])&&(n=e)}),n}();e&&(window.previousBGSize=e,document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(e=>{window.bgObserver.observe(e)}))}window.addEventListener("scroll",()=>{o||(o=!0,requestAnimationFrame(l))},{passive:!0}),window.previousBGSize="Small",window.bgSizes={Small:500,Med:768,Large:1024},window.bgObserver=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){let t=e.target.dataset[`bg${window.previousBGSize}`];e.target.style.backgroundImage=`url(${t})`,e.target.dataset.bgImg=window.previousBGSize,window.bgObserver.unobserve(e.target)}})},{root:null,rootMargin:"0px 0px -100px 0px",threshold:0}),c(),window.addEventListener("resize",()=>{window.debouncer.schedule("recalc-max-scroll",()=>{a(),s(window.scrollY||e.scrollTop||0)},20),window.debouncer.schedule("bg-resize",()=>{c()})}),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value},window.focusNextElement=function(){if(document.activeElement&&document.activeElement.form){var e=Array.prototype.filter.call(document.activeElement.form.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'),function(e){return e.offsetWidth>0||e.offsetHeight>0||e===document.activeElement}),t=e.indexOf(document.activeElement);if(t>-1)(e[t+1]||e[0]).focus()}}})(); |
| | |
| | | <?php return array('dependencies' => array(), 'version' => 'c3aa1c027f932096017e'); |
| | | <?php return array('dependencies' => array(), 'version' => 'b9d79a303cb5aad9e29a'); |
| | |
| | | (()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=null!=e?e:this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");if(!l)continue;let h=window.decodeHTMLEntities(r.title);[l.href,l.title,l.textContent]=[r.url,`See more ${h}`,h],e.append(n)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})(); |
| | | (()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".filters",content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(e)})}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0]??null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{const i=t.dataset.for?.split(",")??[];t.hidden=i.length>0&&!i.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){let t=this.ui.orderbyWrap.dataset.forOrder.split(",")??[];this.ui.orderbyWrap.hidden=!t.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}processCachedFilters(){Object.keys(this.filters).forEach(e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)})}processURLFilters(){if(!this.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=!this.store.lastResponse?.has_more??!0))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=e??this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),this.store.lastResponse?.has_more??!1)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){let s=i.images[t]??!1;s&&([e.src,e.srcset,e.alt]=[s.tiny,`${s.tiny} 50w, ${s.small} 300w, ${s.medium} 1024w`,s["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){let r=t.taxonomies[i][s]??!1;if(!r)continue;let a=o.cloneNode(!0),n=a.querySelector("a");if(!n)continue;let l=window.decodeHTMLEntities(r.title);[n.href,n.title,n.textContent]=[r.url,`See more ${l}`,l],e.append(a)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${r.fields.post_title??"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.feedBlock=new e)})})})(); |
| | |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2> |
| | | </summary> |
| | | <div class="columns stack-small"> |
| | |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <h2>Learn More About <?=$current->name?></h2> |
| | | </summary> |
| | | <div class="map"> |
| | |
| | | <?= apply_filters('wpautop', $comment) ?> |
| | | </div> |
| | | <?php } ?> |
| | | <cite class="row start nowrap"> |
| | | <cite class="row left nowrap"> |
| | | <?php if (!empty($profilePhoto)) { ?> |
| | | <img src="<?=esc_url($profilePhoto)?>" |
| | | alt="<?=esc_attr($reviewer)?>" |
| | |
| | | </div> |
| | | <?php } ?> |
| | | |
| | | <div class="row start wrap"> |
| | | <div class="row left wrap"> |
| | | <?php if ($showRating && $rating > 0) { ?> |
| | | <div class="stars" title="<?= $rating ?> out of 5 stars"> |
| | | <?php |
| | |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2> |
| | | </summary> |
| | | <div class="columns stack-small"> |
| | |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <h2>Learn More About <?=$current->name?></h2> |
| | | </summary> |
| | | <div class="map"> |
| | |
| | | |
| | | <?php foreach ($results['details'] as $taxonomy => $detail): ?> |
| | | <details> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <strong><?= esc_html(ucfirst($taxonomy)) ?></strong> |
| | | <?php if ($detail['errors'] === 0): ?> |
| | | <span style="color: green;">✓ Success</span> |
| | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\LoginManager; |
| | | use JVBase\managers\SEO\BreadcrumbManager; |
| | | use JVBase\utility\Image; |
| | | use WP_Block; |
| | | use WP_Query; |
| | | |
| | |
| | | class CustomBlocks |
| | | { |
| | | protected Cache $cache; |
| | | protected array $shouldRender = ['core/query']; |
| | | |
| | | protected static ?WP_Query $currentLoop = null; |
| | | protected static ?int $currentQueryId = null; |
| | | protected static array $counters = []; |
| | | protected static ?WP_Query $originalQuery = null; |
| | | protected array $ignore = ['align','alt','area','backgroundColor','borderColor','buttonText','buttonPosition','buttonUseIcon','categories','className','columns','contentPosition','customOverlayColor','dimRatio','displayAsDropdown','displayAuthor','displayFeaturedImage','displayPostContent','displayPostContentRadio','displayPostDate','excerptLength','featuredImageAlign','fontSize','gradient','height','iconColor','iconColorValue','iconColorValue','iconBackgroundColor','iconBackgroundColorValue','id','imageFill','isDark','isLink','isSearchFieldHidden','isStackedOnMobile','isUserOverlayColor','kind','label','largestFontSize','layout','level','mediaId','mediaLink','mediaSizeSlug','mediaType','metadata','minHeight','minHeightUnit','opacity','opensInNewTab','order','orderBy','ordered','overlayMenu','placeholder','postLayout','postsToShow','query', 'queryId','ref','rel','shouldSyncIcon','showEmpty','showHierarchy','showLabel','showLabels','showOnlyTopLevel','showPostCounts','showTagCounts','size','sizeSlug','slug','smallestFontSize','tagName','taxonomy','term','textAlign','textColor','theme','title','type','url','useFeaturedImage','width','widthUnit',]; |
| | | |
| | | //For custom style output for nested links, etc |
| | | protected static array $pendingStyles = []; |
| | | protected static array $pendingClass = []; |
| | | public function __construct() |
| | | { |
| | | $this->cache = Cache::for('blocks', WEEK_IN_SECONDS); |
| | |
| | | 'label' => __('Callout Alt', 'jvb') |
| | | ] |
| | | ); |
| | | register_block_style( |
| | | 'core/separator', |
| | | [ |
| | | 'name' =>'logo', |
| | | 'label' => __('With Logo', 'jvb') |
| | | ] |
| | | ); |
| | | } |
| | | protected function checkMethods(?string $content, array $block, ?WP_Block $parent = null, bool $isPrerender = false):?string |
| | | { |
| | |
| | | if (function_exists($function)) { |
| | | return $function($block, $content, $parent); |
| | | } else if (method_exists($this, $method)) { |
| | | return $this->$method($block, $content, $parent); |
| | | $content = $this->$method($block, $content, $parent); |
| | | return $isPrerender ? $this->maybeOutputCustomStyles().$content : $content; |
| | | } elseif (!empty($blockName) && JVB_TESTING) { |
| | | if (!in_array($block['blockName'], $this->getIgnore($isPrerender))) { |
| | | jvbDump('No method found for '.print_r($block['blockName'], true)); |
| | |
| | | ]; |
| | | if ($isPrerender) { |
| | | $base = array_merge($base, [ |
| | | |
| | | 'core/query-pagination', |
| | | 'core/query-pagination-previous', |
| | | 'core/query-pagination-next', |
| | | 'core/query-pagination-numbers', |
| | | 'core/query', |
| | | 'core/calendar', |
| | | 'core/archives', |
| | | ]); |
| | | } else { |
| | | $base = array_merge($base, [ |
| | |
| | | } |
| | | $icon = ''; |
| | | if (str_contains($url[1], 'google.com/maps')) { |
| | | $icon = 'google-logo'; |
| | | $icon = jvbIcon('google-logo'); |
| | | } |
| | | if (str_contains($url[1], 'maps.apple.com')) { |
| | | $icon = 'apple-logo'; |
| | | $icon = jvbIcon('apple-logo'); |
| | | } |
| | | |
| | | if ($icon !== '') { |
| | |
| | | $this->getClassesAndStyles($block['attrs']??[]), |
| | | esc_url($url[1]), |
| | | esc_html($label[1]), |
| | | jvbIcon($icon) |
| | | $icon |
| | | ); |
| | | } |
| | | |
| | |
| | | |
| | | public function prerender_core_columns(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | $tagName = array_key_exists('tagName', $block['attrs']) ? $block['attrs']['tagName'] : 'section'; |
| | | jvbDump($block, 'columns'); |
| | | $attrs = $block['attrs']??[]; |
| | | $tagName = array_key_exists('tagName', $attrs) ? $attrs['tagName'] : 'section'; |
| | | |
| | | $classes = ['row', 'nowrap']; |
| | | if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true){ |
| | | $classes[] = 'stack-small'; |
| | | } |
| | | return sprintf( |
| | | '<%s%s>%s</%s>', |
| | | $tagName, |
| | | $this->getClassesAndStyles($block['attrs']??[], ['row nowrap']), |
| | | $this->getClassesAndStyles($attrs, $classes), |
| | | $this->innerBlocks($block).'</section>', |
| | | $tagName |
| | | ); |
| | |
| | | //core_home_link |
| | | //core_more |
| | | //core_nextpage |
| | | public function prerender_core_nextpage(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | |
| | | return str_replace('</a>', '</a></li>',str_replace('<a', '<li><a', wp_link_pages([ |
| | | 'before' => '<nav class="pagination x-btw"><ul>', |
| | | 'after' => '</ul></nav>', |
| | | 'nextpagelink' => __('<span>Next </span>'.jvbIcon('caret-circle-right'), 'jvb'), |
| | | 'previouspagelink' => __(jvbIcon('caret-circle-left').'<span> Previous</span>', 'jvb'), |
| | | 'next_or_number'=> 'next', |
| | | 'echo' => false |
| | | ]))); |
| | | } |
| | | |
| | | public function prerender_core_separator(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'separator'); |
| | | // jvbDump($parent, 'Parent'); |
| | | return '<hr'.$this->getClassesAndStyles($block['attrs']??[]).'>'; |
| | | $attrs = $block['attrs']??[]; |
| | | $logo = ''; |
| | | if (array_key_exists('className', $attrs) && $attrs['className'] === 'is-style-logo'){ |
| | | $logo = apply_filters('jvbSeparatorLogo', 'logo'); |
| | | if (!empty($logo)) { |
| | | $logo = jvbIcon($logo); |
| | | } |
| | | } |
| | | return sprintf( |
| | | '<hr%s>', |
| | | $this->getClassesAndStyles($attrs), |
| | | // $logo |
| | | ); |
| | | } |
| | | |
| | | public function prerender_core_spacer(array $block, ?string $content, ?WP_Block $parent):?string |
| | |
| | | // jvbDump($block, 'media text'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $ID = $this->imageID('', $block); |
| | | $attrs = $block['attrs']??[]; |
| | | |
| | | $size = array_key_exists('mediaSizeSlug', $block['attrs']??[]) ? $block['attrs']['mediaSizeSlug'] : 'large'; |
| | | $size = array_key_exists('mediaSizeSlug', $attrs) ? $attrs['mediaSizeSlug'] : 'large'; |
| | | $imgLink = ($ID) ? $this->imageLink(true, $ID, 'tiny', $size) : ''; |
| | | |
| | | $inner = $this->innerBlocks($block); |
| | | |
| | | |
| | | $classes = ['media-text', 'row']; |
| | | if (array_key_exists('isStackedOnMobile', $block['attrs']??[])) { |
| | | $classes[] = 'nowrap'; |
| | | $classes = ['media-text', 'row', 'nowrap']; |
| | | if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true) { |
| | | $classes[] = 'stack-small'; |
| | | } |
| | | $content = '<div'.$this->getClassesAndStyles($block['attrs']??[], $classes).'>'; |
| | | $content .= (array_key_exists( |
| | | 'mediaPosition', |
| | | $block['attrs']??[] |
| | | ) && $block['attrs']['mediaPosition'] == 'right') ? |
| | | '<div>'.$inner.'</div><figure>'.$imgLink.'</figure>' : |
| | | '<figure>'.$imgLink.'</figure><div>'.$inner.'</div>'; |
| | | $content .= '</div>'; |
| | | return $content; |
| | | |
| | | $inside = array_key_exists('mediaPosition', $attrs) && $attrs['mediaPosition'] === 'right' |
| | | ? sprintf( |
| | | '<div>%s</div><figure>%s</figure>', |
| | | $inner, $imgLink |
| | | ) : sprintf( |
| | | '<figure>%s</figure><div>%s</div>', |
| | | $imgLink, $inner |
| | | ); |
| | | |
| | | return sprintf( |
| | | '<div%s>%s</div>', |
| | | $this->getClassesAndStyles($attrs, $classes), |
| | | $inside |
| | | ); |
| | | } |
| | | //core_video |
| | | |
| | | public function prerender_core_video(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | jvbDump($block, 'video'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $ID = $this->imageID('', $block); |
| | | if (!$ID) { |
| | | return ''; |
| | | } |
| | | |
| | | jvbDump($ID); |
| | | |
| | | $title = (get_the_title($ID) !== '') ? '<b>'.get_the_title($ID).'</b>' : ''; |
| | | $caption = (wp_get_attachment_caption($ID)) ? |
| | | '<figcaption>' . |
| | | $title . |
| | | wp_get_attachment_caption($ID) . |
| | | '</figcaption>' : |
| | | '<figcaption>' . $title . '</figcaption>'; |
| | | $size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large'; |
| | | return '<figure'. |
| | | $this->getClassesAndStyles($block['attrs']??[]).'>'. |
| | | $this->imageLink(true, $ID, 'tiny', $size) . |
| | | $caption.'</figure>'; |
| | | } |
| | | |
| | | /** |
| | | * Reusable blocks |
| | |
| | | //prerender_core_classic |
| | | public function prerender_core_heading(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'heading'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $level = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : '2'; |
| | | $content = $this->innerBlocks($block); |
| | | $content = $this->inside($block); |
| | | $id = sanitize_title(wp_strip_all_tags($this->stripTagContents('small', $content))); |
| | | return '<h'.$level.' id="'.$id.'"'.$this->getClassesAndStyles($block['attrs']??[]).'>'. |
| | | $content. |
| | |
| | | { |
| | | // jvbDump($block, 'paragraph'); |
| | | // jvbDump($parent, 'Parent'); |
| | | return '<p'.$this->getClassesAndStyles($block['attrs']??[]).'>'. |
| | | $this->innerBlocks($block). |
| | | '</p>'; |
| | | $inside = $this->inside($block); |
| | | return empty($inside) ? '' : sprintf( |
| | | '<p%s>%s</p>', |
| | | $this->getClassesAndStyles($block['attrs']??[]), |
| | | $inside |
| | | ); |
| | | } |
| | | public function prerender_core_quote(array $block, ?string $content, ?WP_Block $parent): ?string |
| | | { |
| | |
| | | } |
| | | //core_pattern |
| | | |
| | | public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent):?string |
| | | public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent = null):?string |
| | | { |
| | | // jvbDump($block, 'site logo'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $attrs = $block['attrs']??[]; |
| | | $open = $close = ''; |
| | | |
| | | if (!is_home() && !is_front_page()) { |
| | | $open = '<a href="'.get_home_url().'" rel="home">'; |
| | | if ((!is_home() && !is_front_page()) && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) { |
| | | $open = '<a href="'.get_home_url().'" rel="home" class="logo">'; |
| | | $close = '</a>'; |
| | | } |
| | | $img = get_theme_mod('custom_logo'); |
| | | $img = $this->image($img, 'tiny', 'thumbnail'); |
| | | $img = str_replace('<img', '<img'.$this->getClassesAndStyles($block['attrs']??[]), $img); |
| | | $img = sprintf( |
| | | '<figure%s>%s</figure>', |
| | | $this->getClassesAndStyles($attrs, ['logo']), |
| | | $this->image($img, 'tiny', 'thumbnail') |
| | | ); |
| | | return $open.$img.$close; |
| | | } |
| | | //core_site_title_tagline |
| | | public function prerender_core_site_tagline(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | $tagline = get_bloginfo('description'); |
| | | |
| | | return empty($tagline) ? '' : sprintf( |
| | | '<p%s>%s</p>', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['tagline']), |
| | | $tagline |
| | | ); |
| | | } |
| | | |
| | | public function prerender_core_site_title(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'site title'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $tag = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : 1; |
| | | $attrs = $block['attrs']??[]; |
| | | $tag = (array_key_exists('level', $attrs)) ? $attrs['level'] : 1; |
| | | $tag = ($tag == 0) ? 'p' : 'h'.$tag; |
| | | |
| | | $open = $close = ''; |
| | | if (!is_front_page()) { |
| | | $open = '<a href="' . get_home_url() . '" rel="home">'; |
| | | if (!is_front_page() && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) { |
| | | $open = sprintf( |
| | | '<a href="%s" rel="home">', |
| | | get_home_url() |
| | | ); |
| | | $close = '</a>'; |
| | | } |
| | | $class = ($tag === 'p') ? |
| | |
| | | $this->getClassesAndStyles($block['attrs']??[]); |
| | | |
| | | |
| | | return '<'.$tag.$class.'>'. |
| | | $open. |
| | | get_bloginfo('name'). |
| | | $close. |
| | | '</'.$tag.'>'; |
| | | return sprintf( |
| | | '<%s%s>%s%s%s</%s>', |
| | | $tag, |
| | | $class, |
| | | $open, |
| | | get_bloginfo('name'), |
| | | $close, |
| | | $tag |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | { |
| | | // jvbDump($block, 'navigation'); |
| | | // jvbDump($parent, 'Parent'); |
| | | // jvbDump($block, 'navigation'); |
| | | $ID = (array_key_exists('ref', $block['attrs']??[])) ? $block['attrs']['ref'] : false; |
| | | |
| | | if (empty($block['innerBlocks']) && $ID && get_post($ID)) { |
| | | $block['innerBlocks'] = parse_blocks(get_post($ID)->post_content); |
| | | } |
| | | $attrs = $block['attrs']??[]; |
| | | |
| | | $toggle = (array_key_exists('overlayMenu', $block['attrs']??[]) |
| | | && $block['attrs']['overlayMenu'] == 'never') ? |
| | | '': |
| | | '<button class="toggle main" |
| | | data-action="toggle-menu" |
| | | aria-label="Open Menu" |
| | | aria-controls="navigation-' .$ID. '" |
| | | aria-expanded="false">'. |
| | | jvbIcon('list', ['title'=>'Toggle Menu']). |
| | | jvbIcon('x', ['title'=>'Toggle Menu']). |
| | | '</button>'; |
| | | $class = ($toggle === '') ? |
| | | $this->getClassesAndStyles($block['attrs']??[], ['mobile']) : |
| | | $this->getClassesAndStyles($block['attrs']??[]); |
| | | $helpmenu = (get_the_title($ID) === 'Main') ? |
| | | '<nav><ul>'.jvbNotificationMenu().jvbHelpMenu().'</ul></nav>' : |
| | | ''; |
| | | $toggle = ''; |
| | | $classes = []; |
| | | if (!array_key_exists('overlayMenu', $attrs) || $attrs['overlayMenu'] !== 'never') { |
| | | $toggle = sprintf( |
| | | '<button class="toggle main" |
| | | data-action="toggle-menu" |
| | | aria-label="Open Menu" |
| | | aria-controls="navigation-%d" |
| | | aria-expanded="false">%s%s</button>', |
| | | $ID, |
| | | jvbIcon('list'), |
| | | jvbIcon('x') |
| | | ); |
| | | $classes[] = 'mobile'; |
| | | if (array_key_exists('overlayMenu', $attrs) && $attrs['overlayMenu'] === 'always') { |
| | | $classes[] = 'always'; |
| | | } |
| | | } |
| | | if (!array_key_exists('layout', $attrs)) { |
| | | $classes[] = 'left'; |
| | | $classes[] = 'row'; |
| | | } |
| | | $class = $this->getClassesAndStyles($attrs, $classes); |
| | | |
| | | $helpmenu = ''; |
| | | $title = get_the_title($ID); |
| | | $isMain = false; |
| | | if ($title === 'Main') { |
| | | $isMain = true; |
| | | $helpmenu = sprintf( |
| | | '<nav><ul>%s%s</ul></nav>', |
| | | jvbNotificationMenu(), |
| | | jvbHelpMenu() |
| | | ); |
| | | } |
| | | |
| | | |
| | | //Allows to add custom items to a menu, based on the menu name |
| | | $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, get_the_title($ID)); |
| | | $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), get_the_title($ID), $block)); |
| | | $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, $title, $ID); |
| | | $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), $title, $block)); |
| | | |
| | | $main = str_starts_with($main, '<ul') ? $main : '<ul>'.$main.'</ul>'; |
| | | $main = str_starts_with($main, '<ul') ? $main : sprintf('<ul>%s</ul>',$main); |
| | | |
| | | return '<nav'.$class.' id="navigation-' . $ID . '"aria-label="Navigation"> |
| | | <span class="screen-reader-text"> |
| | | $skipToContent = $isMain ? '<span class="screen-reader-text"> |
| | | <a href="#content">Skip to Content</a> |
| | | </span>' . |
| | | $toggle . |
| | | $main. |
| | | '</nav>'.$helpmenu; |
| | | </span>' : ''; |
| | | return sprintf( |
| | | '<nav%s id="navigation-%d"aria-label="Navigation"> |
| | | %s%s%s</nav>%s', |
| | | $class, |
| | | $ID, |
| | | $skipToContent, |
| | | $toggle, |
| | | $main, |
| | | $helpmenu |
| | | ); |
| | | } |
| | | |
| | | public function prerender_core_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string |
| | |
| | | if (!array_key_exists('attrs', $block)) { |
| | | return ''; |
| | | } |
| | | $url = (str_starts_with($block['attrs']['url'],'/')) ? |
| | | home_url($block['attrs']['url']) : |
| | | $block['attrs']['url']; |
| | | $attrs = $block['attrs']??[]; |
| | | $url = (str_starts_with($attrs['url'],'/')) ? |
| | | home_url($attrs['url']) : |
| | | $attrs['url']; |
| | | $current = (home_url($wp->request.'/') == $url); |
| | | $temp = $block['attrs']??[]; |
| | | unset($temp['url']); |
| | | $attrs['url'] = $url; |
| | | $classes = ($current) ? |
| | | $this->getClassesAndStyles($temp, ['current']): |
| | | $this->getClassesAndStyles($temp); |
| | | $this->getClassesAndStyles($attrs, ['current']): |
| | | $this->getClassesAndStyles($attrs); |
| | | $aria = ''; |
| | | if ($current) { |
| | | $aria = ' aria-current="page"'; |
| | | } |
| | | $linkOpen = $this->build_navigation_link($block['attrs']??[], $aria); |
| | | $linkOpen = $this->buildNavigationLink($attrs, $aria); |
| | | |
| | | |
| | | return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>'; |
| | |
| | | $block['attrs']['url']; |
| | | $current = (home_url($wp->request) == $url); |
| | | |
| | | $temp = $block['attrs']??[]; |
| | | unset($temp['url']); |
| | | $attrs = $block['attrs']??[]; |
| | | $attrs['url'] = $url; |
| | | $classes = ($current) ? |
| | | $this->getClassesAndStyles($temp, ['has-submenu', 'current']): |
| | | $this->getClassesAndStyles($temp, ['has-submenu']); |
| | | $this->getClassesAndStyles($attrs, ['has-submenu', 'current']): |
| | | $this->getClassesAndStyles($attrs, ['has-submenu']); |
| | | |
| | | $aria = ''; |
| | | if ($current) { |
| | | $aria = ' aria-current="page"'; |
| | | } |
| | | $id = sanitize_title($block['attrs']['label']); |
| | | $linkOpen = $this->build_navigation_link($block['attrs'], $aria); |
| | | $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label']. |
| | | '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'. |
| | | jvbIcon('caret-down', ['title'=>'Toggle Submenu']). |
| | | '</button><ul class="submenu" id='.$id.'-submenu">'; |
| | | $linkOpen = $this->buildNavigationLink($attrs, $aria); |
| | | $content = sprintf( |
| | | '<li%s>%s%s</a> |
| | | <button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open %s Submenu" aria-expanded="false" aria-controls="%s-submenu"> |
| | | %s |
| | | </button> |
| | | <ul class="submenu" id=%s-submenu">', |
| | | $classes, |
| | | $linkOpen, |
| | | $attrs['label'], |
| | | $attrs['label'], |
| | | $id, |
| | | jvbIcon('caret-down', ['title'=>'Toggle Submenu']), |
| | | $id |
| | | ); |
| | | |
| | | $content .= $this->innerBlocks($block); |
| | | $content .= '</ul></li>'; |
| | |
| | | return $content; |
| | | } |
| | | |
| | | protected function build_navigation_link(array $attrs, string $aria):string |
| | | protected function buildNavigationLink(array $attrs, string $aria):string |
| | | { |
| | | global $wp; |
| | | $url =(str_starts_with($attrs['url'],'/')) ? |
| | | home_url($attrs['url']) : |
| | | $attrs['url']; |
| | |
| | | * Theme Query Blocks |
| | | */ |
| | | //core_post_author |
| | | |
| | | public function prerender_core_post_author(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | $attrs = $block['attrs'] ?? []; |
| | | |
| | | $size = 96; |
| | | if (array_key_exists('avatarSize',$attrs) && is_int($attrs['avatarSize'])) { |
| | | $size = $attrs['avatarSize']; |
| | | } |
| | | $byline = $aOpen = $aClose = $avatar = $bio = ''; |
| | | global $post; |
| | | $user = get_userdata($post->post_author); |
| | | |
| | | if (!array_key_exists('showAvatar', $attrs) || $this->checkAttrs('showAvatar', $attrs)){ |
| | | $avatar = get_avatar($post->post_author, $size); |
| | | } |
| | | if (!array_key_exists('showBio', $attrs) || $this->checkAttrs('showBio', $attrs)) { |
| | | $bio = wpautop($user->description); |
| | | } |
| | | |
| | | $target = ''; |
| | | if (array_key_exists('linkTarget', $attrs) && $attrs['linkTarget']=== '_blank') { |
| | | $target = ' target="_blank"'; |
| | | } |
| | | |
| | | if ($this->checkAttrs('isLink', $attrs)) { |
| | | $aOpen = sprintf( |
| | | '<a href="%s"%s>', |
| | | get_author_posts_url($post->post_author), |
| | | $target |
| | | ); |
| | | $aClose = '</a>'; |
| | | } |
| | | |
| | | if (array_key_exists('byline', $attrs)) { |
| | | $byline = sprintf( |
| | | '<small>%s</small> — ', |
| | | $attrs['byline'] |
| | | ); |
| | | } |
| | | |
| | | $name = $user->display_name; |
| | | |
| | | |
| | | return sprintf( |
| | | '<div%s>%s%s%s<p>%s%s%s%s</p>%s</div>', |
| | | $this->getClassesAndStyles($attrs, ['row','nowrap']), |
| | | $aOpen, |
| | | $avatar, |
| | | $aClose, |
| | | $aOpen, |
| | | $byline, |
| | | $name, |
| | | $aClose, |
| | | $bio |
| | | ); |
| | | } |
| | | //core_post_author_biography |
| | | //core_post_author_name |
| | | |
| | | public function prerender_core_post_author_name(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | $attrs = $block['attrs']??[]; |
| | | global $post; |
| | | $aOpen = $aClose = ''; |
| | | if ($this->checkAttrs('isLink', $attrs)) { |
| | | |
| | | $aOpen = sprintf( |
| | | '<a href="%s" rel="author">', |
| | | get_author_posts_url($post->post_author) |
| | | ); |
| | | $aClose = '</a>'; |
| | | } |
| | | |
| | | $author = get_userdata($post->post_author); |
| | | return sprintf( |
| | | '<p%s>%s%s%s</p>', |
| | | $this->getClassesAndStyles($attrs, ['author']), |
| | | $aOpen, |
| | | $author->display_name, |
| | | $aClose |
| | | ); |
| | | } |
| | | public function prerender_core_post_content(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'post content'); |
| | |
| | | |
| | | if ($content == '') { |
| | | if(is_singular()) { |
| | | global $post; |
| | | global $post, $page; |
| | | |
| | | $block['innerBlocks'] = parse_blocks($post->post_content); |
| | | $pages = explode('<!--nextpage-->', $post->post_content); |
| | | $currentContent = $pages[max(0, $page - 1)] ?? $pages[0]; |
| | | |
| | | |
| | | if ($page > 1 && !str_contains($currentContent, '<!--nextpage-->')) { |
| | | $currentContent = str_replace('<!-- /wp:nextpage -->','', $currentContent); |
| | | $currentContent .= ' |
| | | <!-- wp:nextpage --> |
| | | <!--nextpage--> |
| | | <!-- /wp:nextpage -->'; |
| | | } |
| | | |
| | | $block['innerBlocks'] = parse_blocks($currentContent); |
| | | $result = $this->innerBlocks($block); |
| | | }else { |
| | | $result = ''; |
| | |
| | | //core_post_date |
| | | public function prerender_core_post_date(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | |
| | | // jvbDump($block, 'post date'); |
| | | // return null; |
| | | $attrs = $block['attrs']??[]; |
| | | $postDate = null; |
| | | $itemProp = 'datePublished'; |
| | | $format = array_key_exists('format', $attrs) ? $attrs['format'] : 'M d, Y'; |
| | | $dateFormat = null; |
| | | if (array_key_exists('displayType', $attrs)) { |
| | | switch ($attrs['displayType']) { |
| | | case 'displayType': |
| | | $postDate = get_post_modified_time('c'); |
| | | $dateFormat = get_post_modified_time($format); |
| | | $itemProp = 'dateModified'; |
| | | break; |
| | | |
| | | |
| | | } |
| | | } |
| | | $postDate = is_null($postDate) ? get_the_date('c') : $postDate; |
| | | $dateFormat = is_null($dateFormat) ? get_the_date($format) : $dateFormat; |
| | | |
| | | |
| | | |
| | | $aOpen = $aClose = ''; |
| | | if ($this->checkAttrs('isLink', $attrs) && !is_singular()) { |
| | | $aOpen = sprintf( |
| | | '<a href="%s">', |
| | | get_the_permalink() |
| | | ); |
| | | $aClose = '</a>'; |
| | | } |
| | | // jvbDump($parent, 'Parent'); |
| | | $postDate = get_the_date('c'); |
| | | return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']??[]).'>'.get_the_date().'</time>'; |
| | | |
| | | return sprintf( |
| | | '<time datetime="%s" itemprop="%s"%s>%s%s%s</time>', |
| | | $postDate, |
| | | $itemProp, |
| | | $this->getClassesAndStyles($attrs), |
| | | $aOpen, |
| | | $dateFormat, |
| | | $aClose |
| | | ); |
| | | } |
| | | //core_post_excerpt |
| | | public function prerender_core_post_excerpt(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | return wpautop(get_the_excerpt()); |
| | | $attrs = $block['attrs']??[]; |
| | | |
| | | $moreText = array_key_exists('moreText', $attrs) ? $attrs['moreText'] : 'Read more '.jvbIcon('arrow-circle-right'); |
| | | $showMoreOnNewLine = !array_key_exists('showMoreOnNewLine', $attrs) || $this->checkAttrs('showMoreOnNewLine', $attrs); |
| | | // jvbDump($block); |
| | | // jvbDump($showMoreOnNewLine); |
| | | |
| | | $excerpt = array_filter(explode('<p>',wpautop(get_the_excerpt()))); |
| | | $classes = $this->getClassesAndStyles($attrs); |
| | | $excerpt = array_map(function ($line) use ($classes) { |
| | | return sprintf( |
| | | '<p%s>%s', |
| | | $classes, |
| | | $line |
| | | ); |
| | | }, $excerpt); |
| | | |
| | | if (!empty($moreText)) { |
| | | if ($showMoreOnNewLine) { |
| | | $excerpt[] = sprintf( |
| | | '<p%s><a href="%s" class="read-more">%s</a></p>', |
| | | $classes, |
| | | get_the_permalink(), |
| | | $moreText |
| | | ); |
| | | } else { |
| | | $last = array_key_last($excerpt); |
| | | $excerpt[$last] = str_replace('</p>', sprintf('<a href="%s" class="read-more">%s</a>', |
| | | get_the_permalink(), |
| | | $moreText), $excerpt[$last]); |
| | | } |
| | | } |
| | | return implode('',$excerpt); |
| | | } |
| | | public function prerender_core_post_featured_image(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'featured image'); |
| | | // jvbDump($parent, 'Parent'); |
| | | global $post; |
| | | $attrs = $block['attrs']??[]; |
| | | $ID = get_post_thumbnail_id($post->ID); |
| | | $aOpen = $aClose = ''; |
| | | if(!is_single($post->ID)) { |
| | | $aspectRatio = $aOpen = $aClose = ''; |
| | | if(!is_single($post->ID) && $this->checkAttrs('isLink', $attrs)) { |
| | | $aOpen = '<a href="'.get_the_permalink($post->ID).'">'; |
| | | $aClose = '</a>'; |
| | | } |
| | | if (array_key_exists('aspectRatio', $attrs)) { |
| | | $aspectRatio = $attrs['aspectRatio']; |
| | | } |
| | | |
| | | return '<figure'.$this->getClassesAndStyles($block['attrs']??[]).'>'.$aOpen. |
| | | apply_filters('jvbCoreFeaturedImage', $this->image($ID), $post->post_type). |
| | | $aClose.'</figure>'; |
| | | $img = apply_filters('jvbCoreFeaturedImage', '', $post->post_type, $attrs); |
| | | |
| | | if (empty($img)) { |
| | | $img = $this->image($ID); |
| | | $img = empty($aspectRatio) ? $img : str_replace('<img', '<img style="aspect-ratio:'.$aspectRatio.';"', $img); |
| | | } |
| | | |
| | | |
| | | return !empty($img) ? sprintf( |
| | | '<figure%s>%s%s%s</figure>', |
| | | $this->getClassesAndStyles($attrs), |
| | | $aOpen, |
| | | $img, |
| | | $aClose, |
| | | ):''; |
| | | } |
| | | //core_post_navigation_link |
| | | public function prerender_core_post_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string |
| | |
| | | ); |
| | | } |
| | | //core_post_template |
| | | public function render_core_post_template(array $block, string $content):string |
| | | public function prerender_core_post_template(array $block, ?string $content):?string |
| | | { |
| | | global $wp_query; |
| | | |
| | | $inner = ''; |
| | | $block['innerBlocks'][0]['attrs']['tagName'] = 'li'; |
| | | if ($wp_query->have_posts()) { |
| | | while ($wp_query->have_posts()) { |
| | | $wp_query->the_post(); |
| | | |
| | | $inner .= $this->innerBlocks($block); |
| | | } |
| | | wp_reset_postdata(); |
| | | if (!static::$currentLoop) { |
| | | jvbDump('No loop stored'); |
| | | return $content; |
| | | } |
| | | if (static::$currentLoop->have_posts()) { |
| | | while (static::$currentLoop->have_posts()) { |
| | | static::$currentLoop->the_post(); |
| | | |
| | | $inner .= sprintf( |
| | | '<li>%s</li>', |
| | | $this->innerBlocks($block, '','',$block) |
| | | ); |
| | | } |
| | | } |
| | | return sprintf( |
| | | '<ul class="loop">%s</ul>', |
| | | '<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['loop']), |
| | | $inner |
| | | ); |
| | | } |
| | |
| | | return ''; |
| | | } |
| | | $terms = get_the_terms(get_the_ID(), $block['attrs']['term']); |
| | | $attrs = $block['attrs']??[]; |
| | | $out = ''; |
| | | if ($terms && !is_wp_error($terms)) { |
| | | $out = sprintf( |
| | | '<ul%s>', |
| | | $this->getClassesAndStyles($block['attrs'], ['term-list', 'row', 'start']) |
| | | $this->getClassesAndStyles($attrs, ['term-list', 'row', 'left']) |
| | | ); |
| | | if (array_key_exists('prefix', $block['attrs']??[])) { |
| | | if (array_key_exists('prefix', $attrs)) { |
| | | $out .= sprintf( |
| | | '<li class="prefix">%s</li>', |
| | | $block['attrs']['prefix'] |
| | | $attrs['prefix'] |
| | | ); |
| | | } |
| | | foreach($terms as $term) { |
| | |
| | | html_entity_decode($term->name) |
| | | ); |
| | | } |
| | | if (array_key_exists('suffix', $block['attrs'])) { |
| | | if (array_key_exists('suffix', $attrs)) { |
| | | $out .= sprintf( |
| | | '<li class="suffix">%s</li>', |
| | | $block['attrs']['suffix'] |
| | | $attrs['suffix'] |
| | | ); |
| | | } |
| | | $out .= '</ul>'; |
| | |
| | | //core_post_time_to_read |
| | | public function prerender_core_post_title(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'post content'); |
| | | // jvbDump($parent, 'Parent'); |
| | | $open = $close = ''; |
| | | if (array_key_exists('isLink', $block['attrs']??[])) { |
| | | $rel = (array_key_exists('rel', $block['attrs']??[])) ? |
| | | $attrs = $block['attrs']??[]; |
| | | if ($this->checkAttrs('isLink', $attrs)) { |
| | | $rel = (array_key_exists('rel', $attrs)) ? |
| | | ' rel="'.$block['attrs']['rel'].'"' : |
| | | ''; |
| | | $target = (array_key_exists('linkTarget', $block['attrs']??[])) ? |
| | | $target = (array_key_exists('linkTarget', $attrs)) ? |
| | | ' target="'.$block['attrs']['linkTarget'].'"' : |
| | | ''; |
| | | $open = '<a href="' . get_the_permalink() . '"' . $rel . $target . '>'; |
| | | $close = '</a>'; |
| | | } |
| | | if (is_singular(BASE.'partner')) { |
| | | $open .= '<small>edmonton.ink partner:</small> '; |
| | | } |
| | | $level = (array_key_exists('attrs', $block) && |
| | | array_key_exists('level', $block['attrs'])) ? |
| | | $block['attrs']['level'] : |
| | | 2; |
| | | return '<h'.$level.$this->getClassesAndStyles($block['attrs']??[]).'>'. |
| | | $open.get_the_title().$close. |
| | | '</h'.$level.'>'; |
| | | |
| | | $level = $attrs['level']??2; |
| | | |
| | | $title = (!static::$currentLoop && !is_singular()) |
| | | ? get_the_title(get_queried_object_id()) |
| | | : get_the_title(); |
| | | |
| | | return sprintf( |
| | | '<h%s%s>%s%s%s</h%s>', |
| | | $level, |
| | | $this->getClassesAndStyles($attrs), |
| | | $open, |
| | | $title, |
| | | $close, |
| | | $level |
| | | ); |
| | | } |
| | | |
| | | public function render_core_query(array $block, string $content): string |
| | | public function prerender_core_query(array $block, ?string $content):?string |
| | | { |
| | | global $wp_query; |
| | | $inherit = $block['attrs']['inherit'] ?? false; |
| | | |
| | | // $queryID = $block['attrs']['queryId'] ?? null; |
| | | // $inherit = $block['attrs']['inherit'] ?? false; |
| | | // |
| | | // if ($inherit) { |
| | | // global $wp_query; |
| | | // $loop = $wp_query; |
| | | // } else { |
| | | // $args = []; |
| | | // foreach (($block['attrs']['query'] ?? []) as $key => $value) { |
| | | // if (empty($value)) { |
| | | // continue; |
| | | // } |
| | | // switch ($key) { |
| | | // case 'postType': |
| | | // if ($value === BASE.'progress'){ |
| | | // $args['post_parent'] = 0; |
| | | // } |
| | | // $args['post_type'] = $value; |
| | | // break; |
| | | // case 'perPage': |
| | | // $args['posts_per_page'] = $value; |
| | | // break; |
| | | // case 'orderBy': |
| | | // $args['orderby'] = $value; |
| | | // break; |
| | | // case 'taxQuery': |
| | | // $taxQuery = []; |
| | | // foreach ($value as $tax => $terms) { |
| | | // $taxQuery[] = [ |
| | | // 'taxonomy' => $tax, |
| | | // 'terms' => $terms |
| | | // ]; |
| | | // } |
| | | // if (!empty($taxQuery)) { |
| | | // $args['tax_query'] = $taxQuery; |
| | | // if (count($taxQuery) > 1) { |
| | | // $args['tax_query']['relation'] = 'OR'; |
| | | // } |
| | | // } |
| | | // break; |
| | | // case 'sticky': |
| | | // if ($value === 'ignore') { |
| | | // $args['ignore_sticky_posts'] = true; |
| | | // } else if ($value === 'exclude'){ |
| | | // $args['post__not_in'] = get_option('sticky_posts'); |
| | | // } else if ($value === 'only') { |
| | | // $args['include'] = get_option('sticky_posts'); |
| | | // } |
| | | // break; |
| | | // case 'search': |
| | | // $args['s'] = $value; |
| | | // break; |
| | | // default: |
| | | // $args[$key] = $value; |
| | | // break; |
| | | // |
| | | // } |
| | | // } |
| | | // $search = 'query-' . $queryID; |
| | | // foreach ($_GET as $key => $value) { |
| | | // if (str_contains($key, $search)) { |
| | | // $key = str_replace($search, '', $key); |
| | | // if ($key === 'page') { |
| | | // $args['paged'] = (int)$value; |
| | | // } |
| | | // } |
| | | // } |
| | | // $loop = new WP_Query($args); |
| | | // } |
| | | // |
| | | // $inner = ''; |
| | | // foreach ($block['innerBlocks'] as $innerBlock) { |
| | | // switch ($innerBlock['blockName']) { |
| | | // case 'core/post-template': |
| | | // $inner .= '<section class="item-grid">'; |
| | | // if ($loop->have_posts()) { |
| | | // while ($loop->have_posts()) { |
| | | // $loop->the_post(); |
| | | // $postType = get_post_type(); |
| | | // $inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>'; |
| | | // } |
| | | // } |
| | | // $inner .= '</section>'; |
| | | // break; |
| | | // } |
| | | // } |
| | | // |
| | | // // Reset only after a custom query, not the main query |
| | | // if (!$inherit) { |
| | | // wp_reset_postdata(); |
| | | // } |
| | | if ($inherit) { |
| | | static::$currentLoop = $wp_query; |
| | | } else { |
| | | static::$currentLoop = new WP_Query($this->buildQueryArgs($block['attrs'])); |
| | | } |
| | | static::$currentQueryId = $block['attrs']['queryId'] ?? null; |
| | | |
| | | $tagName = $block['attrs']['tagName'] ?? 'div'; |
| | | // return sprintf( |
| | | // '<%s class="loop">%s</%s>', |
| | | // $tagName, |
| | | // $this->innerBlocks($block), |
| | | // $tagName |
| | | // ); |
| | | return $this->innerBlocks($block); |
| | | static::$originalQuery = $wp_query; |
| | | |
| | | $inside = $this->innerBlocks($block); |
| | | |
| | | if (str_contains($inside, 'loop')) { |
| | | $classes = $this->getClassesAndStyles($block['attrs'] ?? [], ['loop']); |
| | | $classes = str_replace(' class="', '', $classes); |
| | | $classes = strtok($classes, '"'); |
| | | $inside = str_replace('loop', $classes, $inside); |
| | | } |
| | | |
| | | |
| | | static::$currentQueryId = null; |
| | | static::$currentLoop = null; |
| | | |
| | | wp_reset_postdata(); |
| | | return $inside; |
| | | } |
| | | // public function render_core_query(array $block, string $content): string |
| | | // { |
| | | // $inside = $this->innerBlocks($block); |
| | | // if (str_contains($inside, 'loop')) { |
| | | // $classes = $this->getClassesAndStyles($block['attrs']??[], ['loop']); |
| | | // $classes = str_replace(' class="', '', $classes); |
| | | // $classes = strtok($classes, '"'); |
| | | // |
| | | // $inside = str_replace('loop', $classes, $inside); |
| | | // } |
| | | // return $inside; |
| | | // } |
| | | protected function buildQueryArgs(array $attrs): array |
| | | { |
| | | $queryID = $attrs['queryId'] ?? null; |
| | | $args = []; |
| | | foreach (($attrs['query'] ?? []) as $key => $value) { |
| | | if (empty($value)) continue; |
| | | switch ($key) { |
| | | case 'postType': $args['post_type'] = $value; break; |
| | | case 'perPage': $args['posts_per_page'] = $value; break; |
| | | case 'orderBy': $args['orderby'] = $value; break; |
| | | case 'sticky': |
| | | match ($value) { |
| | | 'ignore' => $args['ignore_sticky_posts'] = true, |
| | | 'exclude' => $args['post__not_in'] = get_option('sticky_posts'), |
| | | 'only' => $args['post__in'] = get_option('sticky_posts'), |
| | | default => null |
| | | }; |
| | | break; |
| | | case 'taxQuery': |
| | | $taxQuery = array_map(fn($tax, $terms) => [ |
| | | 'taxonomy' => $tax, 'terms' => $terms |
| | | ], array_keys($value), $value); |
| | | if (count($taxQuery) > 1) $taxQuery['relation'] = 'OR'; |
| | | $args['tax_query'] = $taxQuery; |
| | | break; |
| | | case 'search': $args['s'] = $value; break; |
| | | default: $args[$key] = $value; break; |
| | | } |
| | | } |
| | | |
| | | // Handle pagination from query string |
| | | $search = 'q-' . $queryID.'-'; |
| | | foreach ($_GET as $key => $value) { |
| | | if (str_contains($key, $search) && str_replace($search, '', $key) === 'page') { |
| | | $args['paged'] = (int)$value; |
| | | } |
| | | } |
| | | return $args; |
| | | } |
| | | |
| | | protected function buildPaginationUrl(int $page): string |
| | | { |
| | | $param = 'q-' . static::$currentQueryId . '-page'; |
| | | $url = remove_query_arg($param); |
| | | return $page > 1 ? add_query_arg($param, $page, $url) : $url; |
| | | } |
| | | |
| | | protected function getCurrentPage(): int |
| | | { |
| | | $param = 'q-' . static::$currentQueryId . '-page'; |
| | | return isset($_GET[$param]) ? (int)$_GET[$param] : 1; |
| | | } |
| | | // public function render_core_query(array $block, string $content): string |
| | | // { |
| | | // |
| | | //// $queryID = $block['attrs']['queryId'] ?? null; |
| | | //// $inherit = $block['attrs']['inherit'] ?? false; |
| | | //// |
| | | //// if ($inherit) { |
| | | //// global $wp_query; |
| | | //// $loop = $wp_query; |
| | | //// } else { |
| | | //// $args = []; |
| | | //// foreach (($block['attrs']['query'] ?? []) as $key => $value) { |
| | | //// if (empty($value)) { |
| | | //// continue; |
| | | //// } |
| | | //// switch ($key) { |
| | | //// case 'postType': |
| | | //// if ($value === BASE.'progress'){ |
| | | //// $args['post_parent'] = 0; |
| | | //// } |
| | | //// $args['post_type'] = $value; |
| | | //// break; |
| | | //// case 'perPage': |
| | | //// $args['posts_per_page'] = $value; |
| | | //// break; |
| | | //// case 'orderBy': |
| | | //// $args['orderby'] = $value; |
| | | //// break; |
| | | //// case 'taxQuery': |
| | | //// $taxQuery = []; |
| | | //// foreach ($value as $tax => $terms) { |
| | | //// $taxQuery[] = [ |
| | | //// 'taxonomy' => $tax, |
| | | //// 'terms' => $terms |
| | | //// ]; |
| | | //// } |
| | | //// if (!empty($taxQuery)) { |
| | | //// $args['tax_query'] = $taxQuery; |
| | | //// if (count($taxQuery) > 1) { |
| | | //// $args['tax_query']['relation'] = 'OR'; |
| | | //// } |
| | | //// } |
| | | //// break; |
| | | //// case 'sticky': |
| | | //// if ($value === 'ignore') { |
| | | //// $args['ignore_sticky_posts'] = true; |
| | | //// } else if ($value === 'exclude'){ |
| | | //// $args['post__not_in'] = get_option('sticky_posts'); |
| | | //// } else if ($value === 'only') { |
| | | //// $args['include'] = get_option('sticky_posts'); |
| | | //// } |
| | | //// break; |
| | | //// case 'search': |
| | | //// $args['s'] = $value; |
| | | //// break; |
| | | //// default: |
| | | //// $args[$key] = $value; |
| | | //// break; |
| | | //// |
| | | //// } |
| | | //// } |
| | | //// $search = 'query-' . $queryID; |
| | | //// foreach ($_GET as $key => $value) { |
| | | //// if (str_contains($key, $search)) { |
| | | //// $key = str_replace($search, '', $key); |
| | | //// if ($key === 'page') { |
| | | //// $args['paged'] = (int)$value; |
| | | //// } |
| | | //// } |
| | | //// } |
| | | //// $loop = new WP_Query($args); |
| | | //// } |
| | | //// |
| | | //// $inner = ''; |
| | | //// foreach ($block['innerBlocks'] as $innerBlock) { |
| | | //// switch ($innerBlock['blockName']) { |
| | | //// case 'core/post-template': |
| | | //// $inner .= '<section class="item-grid">'; |
| | | //// if ($loop->have_posts()) { |
| | | //// while ($loop->have_posts()) { |
| | | //// $loop->the_post(); |
| | | //// $postType = get_post_type(); |
| | | //// $inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>'; |
| | | //// } |
| | | //// } |
| | | //// $inner .= '</section>'; |
| | | //// break; |
| | | //// } |
| | | //// } |
| | | //// |
| | | //// // Reset only after a custom query, not the main query |
| | | //// if (!$inherit) { |
| | | //// wp_reset_postdata(); |
| | | //// } |
| | | // |
| | | // $tagName = $block['attrs']['tagName'] ?? 'div'; |
| | | //// return sprintf( |
| | | //// '<%s class="loop">%s</%s>', |
| | | //// $tagName, |
| | | //// $this->innerBlocks($block), |
| | | //// $tagName |
| | | //// ); |
| | | // return $this->innerBlocks($block); |
| | | // } |
| | | |
| | | //core_query_no_results |
| | | public function prerender_core_query_no_results(array $block, ?string $content):?string |
| | | { |
| | | if (!static::$currentLoop || static::$currentLoop->have_posts()) { |
| | | return ''; |
| | | } |
| | | |
| | | $inside = $this->innerBlocks($block); |
| | | return empty($inside) ? '' : sprintf( |
| | | '<div%s>%s</div>', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['no-results']), |
| | | $inside |
| | | ); |
| | | } |
| | | //core_query_pagination |
| | | public function prerender_core_query_pagination(array $block, ?string $content):?string |
| | | { |
| | | return sprintf( |
| | | '<nav%s>%s</nav>', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['pagination', 'condensed','btw']), |
| | | $this->innerBlocks($block) |
| | | ); |
| | | } |
| | | |
| | | //core_query_pagination_next |
| | | |
| | | public function prerender_core_query_pagination_next(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | if (!static::$currentLoop) return ''; |
| | | |
| | | $currentPage = $this->getCurrentPage(); |
| | | $maxPages = static::$currentLoop->max_num_pages; |
| | | |
| | | if ($currentPage >= $maxPages) return ''; |
| | | |
| | | $nextLabel = $rArrow = ''; |
| | | $type = get_post_type_object(get_post_type())->label; |
| | | if ($parent) { |
| | | $attrs = $parent->attributes; |
| | | if (array_key_exists('paginationArrow', $attrs)){ |
| | | $rArrow = match($attrs['paginationArrow']) { |
| | | 'chevron' => jvbIcon('caret-circle-right'), |
| | | default => jvbIcon('arrow-circle-right') |
| | | }; |
| | | } |
| | | if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) { |
| | | |
| | | $nextLabel = 'Next '.$type; |
| | | } |
| | | } else { |
| | | $rArrow = jvbIcon('caret-circle-right'); |
| | | } |
| | | |
| | | $aOpen = sprintf( |
| | | '<a class="nav next" href="%s" title="Next %s">', |
| | | $this->buildPaginationUrl($currentPage + 1), |
| | | $type |
| | | ); |
| | | $aClose = '</a>'; |
| | | return sprintf( |
| | | '%s%s%s%s', |
| | | $aOpen, |
| | | $nextLabel, |
| | | $rArrow, |
| | | $aClose |
| | | ); |
| | | } |
| | | //core_query_pagination_numbers |
| | | public function prerender_core_query_pagination_numbers(array $block, ?string $content):?string |
| | | { |
| | | if (!static::$currentLoop) return ''; |
| | | $currentPage = $this->getCurrentPage(); |
| | | $maxPages = (int)static::$currentLoop->max_num_pages; |
| | | |
| | | $attrs = $block['attrs']??[]; |
| | | if ($maxPages <= 1) return ''; |
| | | |
| | | $midSize = $attrs['midSize'] ?? 2; |
| | | $endSize = 1; |
| | | |
| | | |
| | | $items = ''; |
| | | $gap = false; |
| | | for ($i = 1; $i <= $maxPages; $i++) { |
| | | if (($i <= min($endSize + 1, $maxPages)) || |
| | | ($i >= max(1, $currentPage - $midSize) && $i <= min($maxPages, $currentPage + $midSize)) || |
| | | ($i >= max(1, $maxPages - $endSize) && $i <= $maxPages)) { |
| | | $gap = true; |
| | | $items .= ($i === $currentPage) |
| | | ? sprintf('<li aria-current="page" class="current">%d</li>', $i) |
| | | : sprintf('<li><a href="%s">%d</a></li>', $this->buildPaginationUrl($i), $i); |
| | | } elseif ($gap) { |
| | | $gap = false; |
| | | $items .= sprintf( |
| | | '<li class="dots"><span>%s</span></li>', |
| | | jvbIcon('dots-three') |
| | | ); |
| | | } |
| | | } |
| | | |
| | | return sprintf('<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($attrs, ['row', 'nowrap']), |
| | | $items |
| | | ); |
| | | } |
| | | //core_query_pagination_previous |
| | | |
| | | public function prerender_core_query_pagination_previous(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | |
| | | if (!static::$currentLoop) return ''; |
| | | |
| | | $currentPage = $this->getCurrentPage(); |
| | | $maxPages = static::$currentLoop->max_num_pages; |
| | | |
| | | if ($currentPage <= 1) return ''; |
| | | |
| | | $nextLabel = $rArrow = ''; |
| | | $type = get_post_type_object(get_post_type())->label; |
| | | if ($parent) { |
| | | $attrs = $parent->attributes; |
| | | if (array_key_exists('paginationArrow', $attrs)){ |
| | | $rArrow = match($attrs['paginationArrow']) { |
| | | 'chevron' => jvbIcon('caret-circle-left'), |
| | | default => jvbIcon('arrow-circle-left') |
| | | }; |
| | | } |
| | | if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) { |
| | | |
| | | $nextLabel = 'Previous '.$type; |
| | | } |
| | | } else { |
| | | $rArrow = jvbIcon('caret-circle-left'); |
| | | } |
| | | |
| | | $aOpen = sprintf( |
| | | '<a class="nav prev" href="%s" title="Previous %s">', |
| | | $this->buildPaginationUrl($currentPage - 1), |
| | | $type |
| | | ); |
| | | $aClose = '</a>'; |
| | | return sprintf( |
| | | '%s%s%s%s', |
| | | $aOpen, |
| | | $nextLabel, |
| | | $rArrow, |
| | | $aClose |
| | | ); |
| | | } |
| | | |
| | | public function prerender_core_query_title(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | jvbDump($block); |
| | | jvbDump($block, 'query title'); |
| | | $attr = $block['attrs']; |
| | | $name = ''; |
| | | $showPrefix = $attr['showPrefix']??false; |
| | |
| | | $before = apply_filters('jvbAboveHeader', ''); |
| | | if (!empty($before)) { |
| | | $before = sprintf( |
| | | '<aside class="pre header row btw">%s</aside>', |
| | | '<aside class="pre header row x-btw">%s</aside>', |
| | | $before |
| | | ); |
| | | } |
| | |
| | | $after = apply_filters('jvbBelowHeader', $after); |
| | | if (!empty($after)) { |
| | | $after = sprintf( |
| | | '<aside class="sub header row btw">%s</aside>', |
| | | '<aside class="sub header row x-btw">%s</aside>', |
| | | $after |
| | | ); |
| | | } |
| | |
| | | // |
| | | // $beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader); |
| | | // if ($beforeHeader !== '') { |
| | | // $beforeHeader = '<aside class="pre header row btw">'.$beforeHeader.'</aside>'; |
| | | // $beforeHeader = '<aside class="pre header row x-btw">'.$beforeHeader.'</aside>'; |
| | | // } |
| | | // $themeSwitch = jvbDarkModeToggle(); |
| | | // $breadcrumbs = BreadcrumbManager::getInstance()->renderNavigation(); |
| | | // $afterHeader = apply_filters('jvbBelowHeader', $afterHeader); |
| | | // |
| | | // if ($afterHeader !== '') { |
| | | // $afterHeader = '<aside class="sub header row btw">'.$afterHeader.'</aside>'; |
| | | // $afterHeader = '<aside class="sub header row x-btw">'.$afterHeader.'</aside>'; |
| | | // } |
| | | // $footerText = '<div class="scroll-progress"><div class="bar"></div> |
| | | //</div>'; |
| | |
| | | * Widgets Blocks |
| | | */ |
| | | //core_archives |
| | | public function render_core_archives(array $block, string $content):string |
| | | { |
| | | jvbDump($block, 'archives'); |
| | | $attrs = $block['attrs']??[]; |
| | | $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs); |
| | | |
| | | $replace = strtok($content,'>').'>'; |
| | | $content = str_replace($replace, '', $content); |
| | | |
| | | if ($isDropdown) { |
| | | $content = sprintf( |
| | | '<div%s>%s', |
| | | $this->getClassesAndStyles($attrs, ['archive dropdown']), |
| | | $content |
| | | ); |
| | | } else { |
| | | $content = sprintf( |
| | | '<ul%s>%s', |
| | | $this->getClassesAndStyles($attrs, ['archive-list']), |
| | | $content |
| | | ); |
| | | } |
| | | |
| | | return $content; |
| | | } |
| | | //core_calendar |
| | | public function render_core_calendar(array $block, string $content):string |
| | | { |
| | | $content = $this->inside($block, false, $content); |
| | | $replace = strtok($content, '>').'>'; |
| | | $content = str_replace($replace, '', $content); |
| | | return sprintf( |
| | | '<table%s>%s', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['calendar']), |
| | | $content |
| | | ); |
| | | } |
| | | //core_categories |
| | | public function prerender_core_categories(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | $attrs = $block['attrs']??[]; |
| | | $args = [ |
| | | 'taxonomy' => 'category', |
| | | 'hide_empty' => !$this->checkAttrs('showEmpty', $attrs) |
| | | ]; |
| | | |
| | | $showHierarchy = $this->checkAttrs('showHierarchy', $attrs); |
| | | if ($this->checkAttrs('showOnlyTopLevel', $attrs) || $showHierarchy){ |
| | | $args['parent'] = 0; |
| | | } |
| | | |
| | | $terms = $this->getTerms($args, $showHierarchy); |
| | | if (!$terms){ |
| | | return ''; |
| | | } |
| | | |
| | | |
| | | $showPostCounts = $this->checkAttrs('showPostCounts', $attrs); |
| | | $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs); |
| | | |
| | | if ($isDropdown) { |
| | | $this->counter('core_categories'); |
| | | } |
| | | |
| | | $tax = get_taxonomy($args['taxonomy']); |
| | | $taxonomyName = $tax->label??'Categories'; |
| | | $taxonomySingular = $tax->labels->singular_name??'Category'; |
| | | $inner = $this->buildTermList($terms, $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts); |
| | | if ($isDropdown) { |
| | | return sprintf( |
| | | '<div%s>%s</div>', |
| | | $this->getClassesAndStyles($attrs, ['taxonomy-dropdown']), |
| | | $inner |
| | | ); |
| | | } |
| | | return sprintf( |
| | | '<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($attrs, ['taxonomy-list', jvbNoBase($args['taxonomy'])]), |
| | | $inner |
| | | ); |
| | | } |
| | | public function getTerms(array $args, bool $showHierarchy = false):array|false |
| | | { |
| | | $terms = get_terms($args); |
| | | if (!$terms || is_wp_error($terms)) { |
| | | return false; |
| | | } |
| | | $terms = array_map(function ($term) { |
| | | return (array) $term; |
| | | }, $terms); |
| | | |
| | | if ($showHierarchy) { |
| | | $terms = array_map(function ($term) use ($args) { |
| | | $args['parent'] = $term['term_id']; |
| | | $children = $this->getTerms($args, true); |
| | | $term['children'] = $children?:[]; |
| | | return $term; |
| | | }, $terms); |
| | | } |
| | | |
| | | return $terms; |
| | | } |
| | | protected function buildTermList(array $terms, string $taxonomyName, string $taxonomySingular, bool $isDropdown, bool $showPostCounts, bool $isOpening = true, int $level = 0):string |
| | | { |
| | | $out = ''; |
| | | if ($isOpening) { |
| | | $out = $isDropdown ? |
| | | sprintf( |
| | | '<label for="taxonomy-select-%s">%s</label> |
| | | <select name="%s_name" id="taxonomy-select-%s"><option value="">Select %s</option>', |
| | | static::$counters['core_categories'], |
| | | $taxonomyName, |
| | | str_replace('-', '_',sanitize_title(strtolower($taxonomyName))), |
| | | static::$counters['core_categories'], |
| | | $taxonomyName |
| | | ) : |
| | | ''; |
| | | } elseif (!$isDropdown) { |
| | | $out .= '<ul>'; |
| | | } |
| | | |
| | | |
| | | $prefix = ''; |
| | | if ($isDropdown) { |
| | | $base = ' '; |
| | | for ($i = 1; $i <= $level; $i++) { |
| | | $prefix .= $base; |
| | | } |
| | | $prefix .= empty($prefix) ? '' : '- '; |
| | | } |
| | | |
| | | $theTerms = array_map(function ($term) use ($taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, $prefix, $level) { |
| | | if ($isDropdown) { |
| | | return sprintf( |
| | | '<option value="%s">%s%s%s</option>%s', |
| | | $term['slug'], |
| | | $prefix, |
| | | $term['name'], |
| | | $showPostCounts ? ' ('.$term['count'].')' : '', |
| | | empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1) |
| | | ); |
| | | } |
| | | return sprintf( |
| | | '<li><a href="%s">%s%s</a>%s</li>', |
| | | get_term_link($term['term_id']), |
| | | $term['name'], |
| | | $showPostCounts ? ' <span class="count">'.$term['count'].'</span>' : '', |
| | | empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1) |
| | | ); |
| | | }, $terms); |
| | | |
| | | $out .= implode('', $theTerms); |
| | | |
| | | if ($isOpening) { |
| | | $out .= $isDropdown ? |
| | | '</select>' : |
| | | ''; |
| | | } else if (!$isDropdown) { |
| | | $out .= '</ul>'; |
| | | } |
| | | |
| | | |
| | | return $out; |
| | | } |
| | | //core_html |
| | | //core_latest_comments |
| | | //core_latest_posts |
| | | public function prerender_core_latest_posts(array $block, ?string $content, ?WP_Block $parent):?string { |
| | | $attrs = $block['attrs']??[]; |
| | | // jvbDump($block, 'latest posts'); |
| | | |
| | | $args = []; |
| | | $title = 'Latest Posts'; |
| | | $args['order'] = array_key_exists('order', $attrs) ? strtoupper($attrs['order']) : 'DESC'; |
| | | $args['orderby'] = array_key_exists('orderBy', $attrs) ? $attrs['orderBy'] : 'date'; |
| | | $args['posts_per_page'] = array_key_exists('postsToShow', $attrs) ? $attrs['postsToShow'] : 5; |
| | | |
| | | if (array_key_exists('categories', $attrs)) { |
| | | $list = jvbCommaList(array_column($attrs['categories'], 'name')); |
| | | $args['tax_query'] = []; |
| | | $args['tax_query'][] = [ |
| | | 'taxonomy' => 'category', |
| | | 'terms' => array_column($attrs['categories'], 'id') |
| | | ]; |
| | | $title .= ' in '.$list; |
| | | } |
| | | |
| | | $posts = new WP_Query($args); |
| | | |
| | | if (!$posts->have_posts()) { |
| | | return ''; |
| | | } |
| | | $posts = array_map(function ($post) use ($attrs) { |
| | | $img = $this->checkAttrs('displayFeaturedImage', $attrs) |
| | | ? $this->image(get_post_thumbnail_id($post->ID), 'tiny', 'thumbnail') |
| | | : ''; |
| | | |
| | | $author = $this->checkAttrs('displayAuthor', $attrs) |
| | | ? sprintf( |
| | | '<a href="%s">%s</a>', |
| | | get_author_posts_url($post->post_author), |
| | | get_userdata($post->post_author)->display_name |
| | | ) |
| | | : ''; |
| | | |
| | | $date = $this->checkAttrs('displayPostDate', $attrs) |
| | | ? sprintf( |
| | | '<time datetime="%s">%s</time>', |
| | | date('Y-m-d', strtotime($post->post_date)), |
| | | date_i18n('M j, Y', strtotime($post->post_date)) |
| | | ) |
| | | : ''; |
| | | $authorDate = $author; |
| | | if (!empty($authorDate) && !empty($date)) { |
| | | $authorDate .= ' | '.$date; |
| | | } else if (!empty($date)) { |
| | | $authorDate = $date; |
| | | } |
| | | |
| | | $excerpt = ''; |
| | | if ($this->checkAttrs('displayPostContent', $attrs)) { |
| | | if (array_key_exists('excerptLength', $attrs)) { |
| | | $excerpt = wp_trim_words(get_the_content($post->ID), $attrs['excerptLength'], '...'); |
| | | } else { |
| | | $excerpt = get_the_excerpt($post->ID); |
| | | } |
| | | } |
| | | if (!empty($excerpt)) { |
| | | $excerpt = wpautop($excerpt); |
| | | } |
| | | |
| | | return sprintf( |
| | | '<li>%s<p><a href="%s">%s</a>%s</p>%s</li>', |
| | | $img, |
| | | get_the_permalink($post->ID), |
| | | $post->post_title, |
| | | !empty($authorDate) ? ' <small>— '.$authorDate.'</small>' : '', |
| | | $excerpt |
| | | ); |
| | | }, $posts->posts); |
| | | |
| | | wp_reset_postdata(); |
| | | return sprintf( |
| | | '<ul%s>%s</ul>', |
| | | // $title, |
| | | $this->getClassesAndStyles($attrs, ['post-list']), |
| | | implode('', $posts) |
| | | ); |
| | | } |
| | | //core_page_list |
| | | //core_page_list_item |
| | | //core_rss |
| | | public function prerender_core_page_list(array $block, ?string $content, ?WP_Block $parent):?string{ |
| | | $attrs = $block['attrs']??[]; |
| | | $parent = array_key_exists('parentPageID', $attrs) ? $attrs['parentPageID'] : 0; |
| | | $pages = new WP_Query([ |
| | | 'post_type' => 'page', |
| | | 'posts_per_page' => -1, |
| | | 'parent' => $parent |
| | | ]); |
| | | |
| | | if (!$pages->have_posts()) { |
| | | return ''; |
| | | } |
| | | $inside = []; |
| | | foreach($pages->posts as $page) { |
| | | jvbDump($page); |
| | | $inside[] = sprintf( |
| | | '<li><a href="%s">%s</a>', |
| | | get_the_permalink($page->ID), |
| | | $page->post_title |
| | | ); |
| | | } |
| | | wp_reset_postdata(); |
| | | return sprintf( |
| | | '<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($attrs, ['page-list']), |
| | | implode('',$inside) |
| | | ); |
| | | } |
| | | //core_page_list_item (doesn't seem to be a thing) |
| | | // public function prerender_core_page_list_item(array $block, ?string $content, ?WP_Block $parent):?string{ |
| | | // return $content; |
| | | // } |
| | | //core_ |
| | | // public function prerender_core_rss(array $block, ?string $content, ?WP_Block $parent):?string |
| | | // { |
| | | // jvbDump($block, 'rss'); |
| | | // return $content; |
| | | // } |
| | | //core_search |
| | | public function prerender_core_search(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'search'); |
| | | $attrs = $block['attrs']??[]; |
| | | $label = array_key_exists('label', $attrs) && !empty($attrs['label']) ? $attrs['label'] : ''; |
| | | if (array_key_exists('showLabel', $attrs) && $attrs['showLabel'] === false) { |
| | | $label = ''; |
| | | } |
| | | $placeholder = array_key_exists('placeholder', $attrs) ? $attrs['placeholder'] : 'Search...'; |
| | | |
| | | $buttonText = array_key_exists('buttonText', $attrs) && !empty($attrs['buttonText']) ? $attrs['buttonText'] : ''; |
| | | |
| | | $isInside = array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-inside'; |
| | | |
| | | $hideInput = $this->checkAttrs('isSearchFieldHidden', $attrs) || (array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-only'); |
| | | |
| | | return str_replace('<div class="search-container row left nowrap"', sprintf( |
| | | '<div%s', |
| | | $this->getClassesAndStyles($attrs, ['search-container', 'row', 'left', 'nowrap']) |
| | | ), jvbSearch($placeholder, uniqid(), $label, $buttonText, $isInside, $hideInput)); |
| | | } |
| | | //core_shortcode |
| | | public function prerender_core_social_link(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'social link'); |
| | | // jvbDump($parent, 'Parent'); |
| | | |
| | | $parentAttrs = false; |
| | | if ($parent) { |
| | | $parentAttrs = $parent->attributes; |
| | | } |
| | | $attrs = $block['attrs']??[]; |
| | | $url = $attrs['url']??''; |
| | | $service = $attrs['service']?:''; |
| | |
| | | if (!$icon) { |
| | | $icon = jvbIcon('link'); |
| | | } |
| | | return '<li><a href="'.$url.'" target="_blank" rel="nofollow" title="Find us on '.ucfirst($service).'">'.$icon.'<span class="screen-reader-text">Find us on '.ucfirst($service).'</span></a></li>'; |
| | | $serviceName = $this->getServiceName($service); |
| | | $label = $parentAttrs && (!array_key_exists('className', $parentAttrs) || !str_contains($parentAttrs['className'], 'logos-only')) |
| | | ? sprintf( |
| | | '<span>%s</span>', |
| | | $serviceName |
| | | ) |
| | | : sprintf( |
| | | '<span class="screen-reader-text">Find us on %s</span>', |
| | | $serviceName |
| | | ); |
| | | $pillShaped = $parentAttrs && (array_key_exists('className', $parentAttrs) && str_contains($parentAttrs['className'], 'pill-shape')) |
| | | ? 'style="border-radius:var(--radius-outer);"' |
| | | : ''; |
| | | return sprintf( |
| | | '<li><a href="%s" target="_blank" rel="nofollow" title="Find us on %s"%s>%s%s</a></li>', |
| | | $url, |
| | | $serviceName, |
| | | $pillShaped, |
| | | $icon, |
| | | $label |
| | | ); |
| | | } |
| | | private function getServiceName(string $service) { |
| | | return match($service){ |
| | | 'wordpress' => 'WordPress', |
| | | default => ucfirst($service) |
| | | }; |
| | | } |
| | | public function prerender_core_social_links(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'social links'); |
| | | // jvbDump($block['attrs']??[], 'social links'); |
| | | // jvbDump($parent, 'Parent'); |
| | | |
| | | return '<ul class="socials">'.$this->innerBlocks($block).'</ul>'; |
| | | return sprintf( |
| | | '<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($block['attrs']??[], ['socials']), |
| | | $this->innerBlocks($block, '','',$block) |
| | | ); |
| | | } |
| | | //core_tag_cloud |
| | | public function prerender_core_tag_cloud(array $block, ?string $content, ?WP_Block $parent):?string |
| | | { |
| | | // jvbDump($block, 'tag cloud'); |
| | | $attrs = $block['attrs']??[]; |
| | | $taxonomy = (array_key_exists('taxonomy', $attrs) && !empty($attrs['taxonomy'])) |
| | | ? $attrs['taxonomy'] |
| | | : 'post_tag'; |
| | | $showCounts = $this->checkAttrs('showTagCounts', $attrs); |
| | | |
| | | $terms = get_terms([ |
| | | 'taxonomy' => $taxonomy, |
| | | 'hide_empty' => true, |
| | | ]); |
| | | |
| | | if (!$terms || is_wp_error($terms)) { |
| | | return ''; |
| | | } |
| | | |
| | | $inside = ''; |
| | | |
| | | foreach ($terms as $term) { |
| | | $url = get_term_link($term->term_id, $taxonomy); |
| | | $count = $showCounts ? |
| | | sprintf( |
| | | '<span class="count">%d</span>', |
| | | $term->count |
| | | ) : |
| | | ''; |
| | | $size = match(true) { |
| | | $term->count <= 2 => 'small', |
| | | $term->count <= 5 => 'x-small', |
| | | $term->count <= 10 => 'medium', |
| | | $term->count <= 15 => 'x-medium', |
| | | $term->count <= 20 => 'large', |
| | | $term->count <= 25 => 'x-large', |
| | | $term->count <= 30 => 'xx-large', |
| | | $term->count > 30 => 'xxx-large', |
| | | }; |
| | | $fontSize = 'font-size: var(--txt-'.$size.');'; |
| | | $inside .= sprintf( |
| | | '<li class="%s");"><a href="%s" rel="tag">%s%s</a></li>', |
| | | $size, |
| | | // $fontSize, |
| | | $url, |
| | | $term->name, |
| | | $count |
| | | ); |
| | | } |
| | | return sprintf( |
| | | '<ul%s>%s</ul>', |
| | | $this->getClassesAndStyles($attrs, ['term-list','cloud', jvbNoBase($taxonomy)]), |
| | | $inside |
| | | ); |
| | | } |
| | | |
| | | |
| | | /** |
| | |
| | | return trim($clean); |
| | | } |
| | | |
| | | public function innerBlocks(array $block, string $before = '', string $after = '', bool $prerender = true):string |
| | | public function innerBlocks(array $block, string $before = '', string $after = '', ?array $parent = null):string |
| | | { |
| | | if ($parent) { |
| | | $parent = new WP_Block($parent); |
| | | } |
| | | $content = ''; |
| | | foreach ($block['innerBlocks'] as $b) { |
| | | |
| | | $rendered = $parent |
| | | ? $this->checkMethods(null, $b, $parent, true) |
| | | : render_block($b); |
| | | |
| | | $content .= sprintf('%s%s%s', |
| | | $before, |
| | | render_block($b), |
| | | $rendered, |
| | | $after |
| | | ); |
| | | } |
| | |
| | | $classes[] = 'col'; |
| | | } |
| | | |
| | | |
| | | // Merge with passed classes and styles |
| | | $styles = array_merge($attr_styles, $styles); |
| | | $classes = array_merge($attr_classes, $classes); |
| | | |
| | | if (!empty(static::$pendingClass)) { |
| | | $classes = array_merge($classes, static::$pendingClass); |
| | | static::$pendingClass = []; |
| | | } |
| | | $classes = array_unique($classes); |
| | | $data = $this->getDataset($attrs); |
| | | |
| | | // Build attribute strings |
| | | $class_string = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : ''; |
| | | $style_string = !empty($styles) ? ' style="' . implode(';', $styles) . '"' : ''; |
| | | $data_string = ''; |
| | | if (!empty($data)) { |
| | | foreach ($data as $d => $v) { |
| | | if ($d === 'bg-small') { |
| | | $data_string .= ' data-bg-img'; |
| | | } |
| | | $data_string .= sprintf( |
| | | ' data-%s="%s"', |
| | | $d, |
| | | $v |
| | | ); |
| | | } |
| | | } |
| | | |
| | | $return = trim($class_string . $style_string); |
| | | $return = trim($class_string . $style_string . $data_string); |
| | | return ($return=='')? '' : ' '.$return; |
| | | } |
| | | /** |
| | |
| | | $classes = []; |
| | | foreach ($attrs as $key => $value) { |
| | | $class = $this->getClass($key, $value, $attrs); |
| | | if (is_array($class)) { |
| | | $classes = array_merge($classes, $class); |
| | | } else { |
| | | $classes[] = $class; |
| | | } |
| | | if (is_string($class)) { |
| | | $class = explode(' ', $class); |
| | | } |
| | | $classes = array_merge($classes, $class); |
| | | } |
| | | return array_filter($classes, function ($class) { |
| | | return array_unique(array_filter($classes, function ($class) { |
| | | return $class!=='' && !str_starts_with($class, 'wp'); |
| | | }); |
| | | })); |
| | | } |
| | | protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array |
| | | { |
| | | //TODO: gradient |
| | | switch ($key) { |
| | | //Any additional classes the user adds |
| | | case 'className': |
| | |
| | | default => str_replace('is-style-', '', $value), |
| | | }; |
| | | case 'contentPosition': |
| | | |
| | | $classes = []; |
| | | $pos = explode(' ', $value); |
| | | foreach($pos as $p) { |
| | | switch ($p) { |
| | | case 'top': |
| | | $classes[] = 'a-start'; |
| | | break; |
| | | case 'right': |
| | | $classes[] = 'end'; |
| | | break; |
| | | case 'bottom': |
| | | $classes[] = 'a-end'; |
| | | break; |
| | | case 'left': |
| | | $classes[] = 'start'; |
| | | break; |
| | | } |
| | | } |
| | | return implode(' ', $classes); |
| | | return $this->getContentPosition($value); |
| | | case 'term': |
| | | case 'taxonomy': |
| | | return jvbNoBase($value); |
| | | //Layout attributes |
| | | case 'layout': |
| | | $classes = []; |
| | | $type = 'row'; |
| | | if (array_key_exists('type', $value)) { |
| | | $type = 'col'; |
| | | // if ($value['type'] === 'constrained') { |
| | | // $classes[] = 'container col'; |
| | | // } |
| | | } |
| | | if (array_key_exists('orientation', $value)) { |
| | | $type = 'col'; |
| | | if ($value['orientation'] === 'vertical') { |
| | | $classes[] = 'col'; |
| | | if (in_array('row', $classes)) { |
| | | $index = array_search('row', $classes); |
| | | unset($classes[$index]); |
| | | } |
| | | } |
| | | }else if (array_key_exists('type', $value) && $value['type'] === 'flex') { |
| | | $classes[] = 'row'; |
| | | if (in_array('col', $classes)) { |
| | | $index = array_search('col', $classes); |
| | | unset($classes[$index]); |
| | | } |
| | | } |
| | | //jvbDump($type); |
| | | //jvbDump($value); |
| | | // $check = [$value, $attrs]; |
| | | // foreach ($check as $ch) { |
| | | // |
| | | // } |
| | | if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) { |
| | | $classes[] = ($type === 'row') ? 'start' : 'a-start'; |
| | | } |
| | | if (array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) { |
| | | if (in_array($value['justifyContent'], ['left', 'right','space-between'])) { |
| | | // jvbDump($type); |
| | | switch ($value['justifyContent']) { |
| | | case 'right': |
| | | $classes[] = ($type === 'row') ? 'end' : 'a-end'; |
| | | break; |
| | | case 'space-between': |
| | | $classes[] = 'btw'; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | if (array_key_exists('flexWrap', $value)) { |
| | | if ($value['flexWrap'] === 'nowrap') { |
| | | $classes[] = 'nowrap'; |
| | | } |
| | | } |
| | | return implode(' ', $classes); |
| | | return $this->getLayout($value, $attrs); |
| | | case 'align': |
| | | return !empty($value) ? 'align-'.$value : ''; |
| | | case 'verticalAlignment': |
| | | return !empty($value) ? 'v-align-'.$value : ''; |
| | | case 'isStackedMobile': |
| | | switch ($value) { |
| | | case 'bottom': |
| | | $value = 'btm'; |
| | | break; |
| | | case 'center': |
| | | $value = 'y-mid'; |
| | | default: |
| | | } |
| | | return !empty($value) ? $value : ''; |
| | | case 'isStackedOnMobile': |
| | | return ($value === true) ? 'stack-small' : ''; |
| | | case 'justifyContent': |
| | | return !empty($value) ? 'j-'.$value : ''; |
| | | case 'orientation': |
| | | return $value==='column' ? 'column' : ''; |
| | | case 'width': |
| | | return $this->getWidth($value); |
| | | case 'dimRatio': |
| | | if (is_numeric($value)) { |
| | | $width = match (true) { |
| | | $value < 25 => '25', |
| | | $value < 33 => '33', |
| | | $value <= 50 => '50', |
| | | $value < 66 => '66', |
| | | $value < 75 => '75', |
| | | default => 'full', |
| | | }; |
| | | switch ($key) { |
| | | case 'width': |
| | | return 'width-'.$width; |
| | | case 'dimRatio': |
| | | return 'overlay-'.$width; |
| | | } |
| | | } |
| | | return ''; |
| | | |
| | | return $this->getDimRatio($value); |
| | | case 'overlayColor': |
| | | return $value; |
| | | break; |
| | | //Typography |
| | | case 'textAlign': |
| | | return !empty($value) ? 'text-'.$value : ''; |
| | |
| | | |
| | | //Style base: |
| | | case 'style': |
| | | $classes = []; |
| | | //Margin and Padding |
| | | if (array_key_exists('spacing', $value)) { |
| | | foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) { |
| | | if (array_key_exists($search, $value['spacing'])) { |
| | | $directions = []; |
| | | |
| | | // Collect ONLY preset spacing values for classes |
| | | foreach ($value['spacing'][$search] as $direction => $size) { |
| | | $presetSize = $this->getPresetSpacing($size); |
| | | if ($presetSize) { |
| | | $directions[$direction] = $presetSize; |
| | | } |
| | | // Non-preset values are skipped here and handled by inline styles below |
| | | } |
| | | |
| | | if (empty($directions)) { |
| | | continue; |
| | | } |
| | | |
| | | // Check what directions we have |
| | | $hasTop = isset($directions['top']); |
| | | $hasBottom = isset($directions['bottom']); |
| | | $hasLeft = isset($directions['left']); |
| | | $hasRight = isset($directions['right']); |
| | | |
| | | // Check if axes match |
| | | $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right']; |
| | | $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom']; |
| | | |
| | | // All 4 directions exist and match → p-3 |
| | | if ($hasTop && $hasBottom && $hasLeft && $hasRight && |
| | | count(array_unique($directions)) === 1) { |
| | | $classes[] = $c . '-' . reset($directions); |
| | | } |
| | | // Both axes match → px-3 py-2 |
| | | elseif ($xMatch && $yMatch) { |
| | | $classes[] = $c . 'x-' . $directions['left']; |
| | | $classes[] = $c . 'y-' . $directions['top']; |
| | | } |
| | | // Only X axis matches → px-3 (+ individual for top/bottom) |
| | | elseif ($xMatch) { |
| | | $classes[] = $c . 'x-' . $directions['left']; |
| | | if ($hasTop) { |
| | | $classes[] = $c . 't-' . $directions['top']; |
| | | } |
| | | if ($hasBottom) { |
| | | $classes[] = $c . 'b-' . $directions['bottom']; |
| | | } |
| | | } |
| | | // Only Y axis matches → py-3 (+ individual for left/right) |
| | | elseif ($yMatch) { |
| | | $classes[] = $c . 'y-' . $directions['top']; |
| | | if ($hasLeft) { |
| | | $classes[] = $c . 'l-' . $directions['left']; |
| | | } |
| | | if ($hasRight) { |
| | | $classes[] = $c . 'r-' . $directions['right']; |
| | | } |
| | | } |
| | | // No matches - individual directions |
| | | else { |
| | | foreach ($directions as $direction => $size) { |
| | | $dir = match($direction) { |
| | | 'top' => 't', |
| | | 'bottom' => 'b', |
| | | 'left' => 'l', |
| | | 'right' => 'r', |
| | | default => $direction |
| | | }; |
| | | $classes[] = $c . $dir . '-' . $size; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (array_key_exists('fontSize', $value)) { |
| | | if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) { |
| | | $classes[] = 'font-'.$value['fontSize']; |
| | | } |
| | | if (in_array('fontWeight', $value)) { |
| | | $classes[] = 'text-'.$value['fontWeight']; |
| | | } |
| | | if (in_array('textTransform', $value)) { |
| | | if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) { |
| | | $classes[] = $value['textTransform']; |
| | | } |
| | | } |
| | | } |
| | | return implode(' ', $classes); |
| | | return $this->getPresetStyles($value); |
| | | case 'fontSize': |
| | | $classes[] = 'font-'.$value; |
| | | return implode(' ', $classes); |
| | | case 'isStackedOnMobile': |
| | | return ($value === true) ? 'stack-small' : ''; |
| | | case 'width': |
| | | if (is_numeric($value)) { |
| | | $width = match (true) { |
| | | $value < 25 => '25', |
| | | $value < 33 => '33', |
| | | $value <= 50 => '50', |
| | | $value < 66 => '66', |
| | | $value < 75 => '75', |
| | | default => 'full', |
| | | }; |
| | | switch ($key) { |
| | | case 'width': |
| | | return 'width-'.$width; |
| | | case 'dimRatio': |
| | | return 'overlay-'.$width; |
| | | } |
| | | case 'postLayout': |
| | | $classes[] = 'item-grid'; |
| | | if (isset($attrs['columns']) && $attrs['columns']!== 3){ |
| | | $classes[] = sprintf( |
| | | 'split-%d', |
| | | $attrs['columns'] |
| | | ); |
| | | } |
| | | return ''; |
| | | return $classes; |
| | | default: |
| | | $ignore = [ |
| | | 'useFeaturedImage', |
| | | 'opacity', |
| | | 'borderColor', |
| | | 'backgroundColor', |
| | | 'textColor', |
| | | 'minHeight', |
| | | 'minHeightUnit', |
| | | 'isDark', |
| | | 'sizeSlug', |
| | | 'isUserOverlayColor', |
| | | 'customOverlayColor', |
| | | 'dimRatio', |
| | | 'placeholder', |
| | | 'alt', |
| | | 'imageFill', |
| | | 'mediaSizeSlug', |
| | | 'isLink', |
| | | 'kind', |
| | | 'label', |
| | | 'type', |
| | | 'id', |
| | | 'url', |
| | | 'label', |
| | | 'shouldSyncIcon', |
| | | 'rel', |
| | | 'opensInNewTab', |
| | | 'title', |
| | | 'ref', |
| | | 'overlayMenu', |
| | | 'slug', |
| | | 'theme', |
| | | 'tagName', |
| | | 'level', |
| | | 'ordered', |
| | | 'area', |
| | | 'mediaId', |
| | | 'mediaLink', |
| | | 'mediaType', |
| | | 'height', //maybe still need? |
| | | ]; |
| | | if (!is_admin() &&!in_array($key, $ignore)) { |
| | | if (JVB_TESTING && !is_admin() &&!in_array($key, $this->ignore)) { |
| | | // TESTING |
| | | // jvbDump($key, 'getClass'); |
| | | // jvbDump($attrs); |
| | | jvbDump($attrs, '[getClass] '.$key); |
| | | } |
| | | |
| | | return ''; |
| | | } |
| | | } |
| | | /*** CLASS HELPERS ***/ |
| | | private function getContentPosition(string $value):string |
| | | { |
| | | $classes = []; |
| | | $pos = explode(' ', $value); |
| | | foreach($pos as $p) { |
| | | switch ($p) { |
| | | case 'top': |
| | | $classes[] = 'top'; |
| | | break; |
| | | case 'right': |
| | | $classes[] = 'right'; |
| | | break; |
| | | case 'bottom': |
| | | $classes[] = 'btm'; |
| | | break; |
| | | case 'left': |
| | | $classes[] = 'left'; |
| | | break; |
| | | } |
| | | } |
| | | return implode(' ', $classes); |
| | | } |
| | | |
| | | private function getLayout(array $value, array $attrs):array |
| | | { |
| | | // jvbDump($value, 'getLayout'); |
| | | $classes = []; |
| | | |
| | | $type = 'row'; |
| | | $isRow = true; |
| | | //Determine type |
| | | if ((array_key_exists('type', $value) && !in_array($value['type'], ['flex', 'grid'])) || |
| | | (array_key_exists('orientation', $value) && $value['orientation'] === 'vertical')) { |
| | | $type = 'col'; |
| | | $isRow = false; |
| | | } elseif (array_key_exists('type', $value) && $value['type'] === 'grid') { |
| | | $type = 'item-grid'; |
| | | $isRow = false; |
| | | if (array_key_exists('columnCount', $value) && $value['columnCount']!== 3) { |
| | | $classes[] = sprintf( |
| | | 'split-%s', |
| | | $value['columnCount'] |
| | | ); |
| | | } |
| | | } |
| | | |
| | | if (array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) { |
| | | switch ($value['justifyContent']) { |
| | | case 'right': |
| | | $classes[] = 'right'; |
| | | break; |
| | | case 'center': |
| | | $classes[] = 'x-mid'; |
| | | break; |
| | | case 'space-between': |
| | | $classes[] = 'x-btw'; |
| | | break; |
| | | case 'left': |
| | | $classes[] = 'left'; |
| | | break; |
| | | case 'space-evenly': |
| | | $classes[] = 'x-even'; |
| | | break; |
| | | case 'space-around': |
| | | $classes[] = 'x-around'; |
| | | break; |
| | | case 'stretch': |
| | | $classes[] = 'stretch'; |
| | | } |
| | | } else { |
| | | $classes[] = 'left'; |
| | | } |
| | | |
| | | if (array_key_exists('verticalAlignment', $value)) { |
| | | switch ($value['verticalAlignment']) { |
| | | case 'bottom': |
| | | $classes[] = 'btm'; |
| | | break; |
| | | case 'top': |
| | | $classes[] = 'top'; |
| | | break; |
| | | case 'center': |
| | | $classes[] = 'y-mid'; |
| | | break; |
| | | case 'space-between': |
| | | $classes[] = 'y-btw'; |
| | | break; |
| | | case 'space-around': |
| | | $classes[] = 'y-around'; |
| | | break; |
| | | case 'space-even': |
| | | $classes[] = 'y-even'; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | if (array_key_exists('flexWrap', $value)) { |
| | | if ($value['flexWrap'] === 'nowrap') { |
| | | $classes[] = 'nowrap'; |
| | | } |
| | | } |
| | | |
| | | $classes[] = $type; |
| | | return $classes; |
| | | } |
| | | |
| | | private function getWidth(string $value):string |
| | | { |
| | | $value = (int)str_replace('%', '', $value); |
| | | return sprintf( |
| | | 'width-%d', |
| | | match (true) { |
| | | $value <= 25 => '25', |
| | | $value <= 33 => '33', |
| | | $value <= 50 => '50', |
| | | $value <= 66 => '66', |
| | | $value <= 75 => '75', |
| | | default => 'full', |
| | | } |
| | | ); |
| | | } |
| | | private function getDimRatio(string $value):string |
| | | { |
| | | if (is_numeric($value)) { |
| | | return sprintf( |
| | | 'op-%d', |
| | | match (true) { |
| | | $value <= 14 => '1', |
| | | $value <= 28 => '2', |
| | | $value <= 42 => '3', |
| | | $value <= 56 => '45', |
| | | $value <= 70 => '4', |
| | | $value <= 84 => '5', |
| | | default => '6', |
| | | } |
| | | ); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | private function getPresetStyles(array $value):string |
| | | { |
| | | $classes = []; |
| | | //Margin and Padding |
| | | if (array_key_exists('spacing', $value)) { |
| | | $classes = array_merge($classes, $this->buildSpacingClasses($value)); |
| | | } |
| | | if (array_key_exists('color', $value)) { |
| | | if (array_key_exists('duotone', $value['color'])) { |
| | | $preset = explode('|', $value['color']['duotone']); |
| | | $preset = $preset[array_key_last($preset)]; |
| | | if (str_contains($preset, '-')) { |
| | | $preset = explode('-', $preset); |
| | | } else { |
| | | $preset = [$preset]; |
| | | } |
| | | $classes[] = 'duotone'; |
| | | foreach ($preset as $p) { |
| | | $classes[] = $p; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (array_key_exists('fontSize', $value)) { |
| | | if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) { |
| | | $classes[] = 'font-'.$value['fontSize']; |
| | | } |
| | | if (in_array('fontWeight', $value)) { |
| | | $classes[] = 'text-'.$value['fontWeight']; |
| | | } |
| | | if (in_array('textTransform', $value)) { |
| | | if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) { |
| | | $classes[] = $value['textTransform']; |
| | | } |
| | | } |
| | | } |
| | | return implode(' ', $classes); |
| | | } |
| | | private function buildSpacingClasses(array $value):array |
| | | { |
| | | $classes = []; |
| | | foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) { |
| | | if (array_key_exists($search, $value['spacing'])) { |
| | | $directions = []; |
| | | |
| | | // Collect ONLY preset spacing values for classes |
| | | foreach ($value['spacing'][$search] as $direction => $size) { |
| | | $presetSize = $this->getPresetSpacing($size); |
| | | if ($presetSize) { |
| | | $directions[$direction] = $presetSize; |
| | | } |
| | | // Non-preset values are skipped here and handled by inline styles below |
| | | } |
| | | |
| | | if (empty($directions)) { |
| | | continue; |
| | | } |
| | | |
| | | // Check what directions we have |
| | | $hasTop = isset($directions['top']); |
| | | $hasBottom = isset($directions['bottom']); |
| | | $hasLeft = isset($directions['left']); |
| | | $hasRight = isset($directions['right']); |
| | | |
| | | // Check if axes match |
| | | $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right']; |
| | | $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom']; |
| | | |
| | | // All 4 directions exist and match → p-3 |
| | | if ($hasTop && $hasBottom && $hasLeft && $hasRight && |
| | | count(array_unique($directions)) === 1) { |
| | | $classes[] = $c . '-' . reset($directions); |
| | | } |
| | | // Both axes match → px-3 py-2 |
| | | elseif ($xMatch && $yMatch) { |
| | | $classes[] = $c . 'x-' . $directions['left']; |
| | | $classes[] = $c . 'y-' . $directions['top']; |
| | | } |
| | | // Only X axis matches → px-3 (+ individual for top/bottom) |
| | | elseif ($xMatch) { |
| | | $classes[] = $c . 'x-' . $directions['left']; |
| | | if ($hasTop) { |
| | | $classes[] = $c . 't-' . $directions['top']; |
| | | } |
| | | if ($hasBottom) { |
| | | $classes[] = $c . 'b-' . $directions['bottom']; |
| | | } |
| | | } |
| | | // Only Y axis matches → py-3 (+ individual for left/right) |
| | | elseif ($yMatch) { |
| | | $classes[] = $c . 'y-' . $directions['top']; |
| | | if ($hasLeft) { |
| | | $classes[] = $c . 'l-' . $directions['left']; |
| | | } |
| | | if ($hasRight) { |
| | | $classes[] = $c . 'r-' . $directions['right']; |
| | | } |
| | | } |
| | | // No matches - individual directions |
| | | else { |
| | | foreach ($directions as $direction => $size) { |
| | | $dir = match($direction) { |
| | | 'top' => 't', |
| | | 'bottom' => 'b', |
| | | 'left' => 'l', |
| | | 'right' => 'r', |
| | | default => $direction |
| | | }; |
| | | $classes[] = $c . $dir . '-' . $size; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return $classes; |
| | | } |
| | | |
| | | protected function getInlineStyles(array $attrs):array |
| | | { |
| | |
| | | $styles = []; |
| | | foreach ($attrs as $key => $value) { |
| | | $style = $this->getStyle($key, $value, $attrs); |
| | | if (is_string($style)) { |
| | | $style = [$style]; |
| | | } |
| | | $styles = array_merge($styles, $style); |
| | | } |
| | | return $styles; |
| | | } |
| | | protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array |
| | | protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array|string |
| | | { |
| | | $styles = []; |
| | | switch ($key) { |
| | | // Font family settings |
| | | case 'fontFamily': |
| | | if ($value === 'body') { |
| | | $styles[] = 'font-family: "Open Sans", system-ui, -apple-system, sans-serif'; |
| | | } elseif ($value === 'heading') { |
| | | $styles[] = 'font-family: "Josefin Sans", system-ui, -apple-system, sans-serif'; |
| | | } elseif (!empty($value)) { |
| | | $styles[] = 'font-family: '.$value; |
| | | } |
| | | break; |
| | | case 'size': |
| | | return $this->getIconSizeStyle($value); |
| | | |
| | | case 'fontFamily': |
| | | return $this->getFontFamilyStyle($value); |
| | | |
| | | // Icon color (for icon blocks) |
| | | case 'iconColorValue': |
| | | if (!empty($value)) { |
| | | $styles[] = 'color: '.$value; |
| | | } |
| | | break; |
| | | return $this->getColorStyle($value); |
| | | |
| | | // Minimum height settings |
| | | case 'minHeight': |
| | | if (!empty($value) && isset($attrs['minHeightUnit'])) { |
| | | $styles[] = 'min-height: '.$value.$attrs['minHeightUnit']; |
| | | } elseif (!empty($value)) { |
| | | $styles[] = 'min-height: '.$value.'px'; // Default to px if no unit specified |
| | | } |
| | | break; |
| | | return $this->getMinHeightStyle($value, $attrs); |
| | | |
| | | // Background URL (for cover, media blocks) |
| | | case 'url': |
| | | if (!empty($value) && str_starts_with($value, 'http')) { |
| | | $styles[] = 'background-image: url('.$value.')'; |
| | | return 'background-image: url('.$value.')'; |
| | | } |
| | | break; |
| | | |
| | | // Focal point for background images |
| | | case 'focalPoint': |
| | | $x = (array_key_exists('x', $attrs['focalPoint'])) ? $attrs['focalPoint']['x'] * 100 : 'center'; |
| | | $y = (array_key_exists('y', $attrs['focalPoint'])) ? $attrs['focalPoint']['y'] * 100 : 'center'; |
| | | $styles[] = 'background-position:'.$x.' '.$y.';'; |
| | | |
| | | break; |
| | | return $this->getFocalPointStyle($value); |
| | | |
| | | // Complex style object |
| | | case 'style': |
| | | // Border styles |
| | | if (isset($value['border'])) { |
| | | $border = $value['border']; |
| | | return $this->extractStyles($value, $attrs); |
| | | |
| | | if (isset($border['radius'])) { |
| | | $styles[] = 'border-radius: '.$border['radius']; |
| | | } |
| | | |
| | | if (isset($border['width'])) { |
| | | $styles[] = 'border-width: '.$border['width']; |
| | | } |
| | | |
| | | if (isset($border['style']) && isset($border['width']) && !empty($border['style'])) { |
| | | $styles[] = 'border-style: '.$border['style']; |
| | | } |
| | | |
| | | if (isset($border['color'])) { |
| | | $styles[] = 'border-color: '.$border['color']; |
| | | } |
| | | } |
| | | |
| | | // Color styles |
| | | if (isset($value['color'])) { |
| | | $color = $value['color']; |
| | | |
| | | if (isset($color['background'])) { |
| | | $styles[] = 'background-color: '.$color['background']; |
| | | } |
| | | |
| | | if (isset($color['text'])) { |
| | | $styles[] = 'color: '.$color['text']; |
| | | } |
| | | |
| | | if (isset($color['gradient'])) { |
| | | $styles[] = 'background: '.$color['gradient']; |
| | | } |
| | | } |
| | | |
| | | // Layout styles |
| | | if (isset($value['layout'])) { |
| | | foreach ($value['layout'] as $layout => $option) { |
| | | switch ($layout) { |
| | | case 'selfStretch': |
| | | if ($option === 'fixed' && isset($value['layout']['selfStretchValue'])) { |
| | | $styles[] = 'width: '.$value['layout']['selfStretchValue']; |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Typography styles |
| | | if (isset($value['typography'])) { |
| | | $typography = $value['typography']; |
| | | |
| | | if (isset($typography['fontSize'])) { |
| | | $styles[] = 'font-size: '.$typography['fontSize']; |
| | | } |
| | | |
| | | if (isset($typography['fontWeight'])) { |
| | | $styles[] = 'font-weight: '.$typography['fontWeight']; |
| | | } |
| | | |
| | | if (isset($typography['textDecoration'])) { |
| | | $styles[] = 'text-decoration: '.$typography['textDecoration']; |
| | | } |
| | | |
| | | if (isset($typography['textTransform'])) { |
| | | $styles[] = 'text-transform: '.$typography['textTransform']; |
| | | } |
| | | |
| | | if (isset($typography['letterSpacing'])) { |
| | | $styles[] = 'letter-spacing: '.$typography['letterSpacing']; |
| | | } |
| | | |
| | | if (isset($typography['lineHeight'])) { |
| | | $styles[] = 'line-height: '.$typography['lineHeight']; |
| | | } |
| | | } |
| | | |
| | | // Spacing styles |
| | | if (isset($value['spacing'])) { |
| | | $spacing = $value['spacing']; |
| | | |
| | | // Don't duplicate margin/padding that's handled by classes |
| | | // Only add specific CSS values here that wouldn't work well as classes |
| | | if (isset($spacing['margin'])) { |
| | | foreach ($spacing['margin'] as $direction => $size) { |
| | | // If not a preset value, add as inline style |
| | | if (!str_contains($size, 'var:preset')) { |
| | | $styles[] = 'margin-'.$direction.': '.$size; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (isset($spacing['padding'])) { |
| | | foreach ($spacing['padding'] as $direction => $size) { |
| | | // If not a preset value, add as inline style |
| | | if (!str_contains($size, 'var:preset')) { |
| | | $styles[] = 'padding-'.$direction.': '.$size; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | break; |
| | | case 'dimRatio': |
| | | $ratio = (ceil($value /25) *25); |
| | | $s = 'background-color: rgba(var(--base-rgb), '; |
| | | switch ($ratio) { |
| | | case 0: |
| | | $s .= 'var(--rgb-subtle-hover));'; |
| | | break; |
| | | case 25: |
| | | $s .= 'var(--rgb-light));'; |
| | | break; |
| | | case 50: |
| | | $s .= 'var(--rgb-medium));'; |
| | | break; |
| | | default: |
| | | $s .= 'var(--rgb-heavy));'; |
| | | break; |
| | | } |
| | | $styles[] = $s; |
| | | break; |
| | | return $this->getDimRatioStyle($value, $attrs); |
| | | |
| | | // Custom styles (any other attributes that need inline styling) |
| | | case 'backgroundType': |
| | | if ($value === 'video' && isset($attrs['backgroundUrl'])) { |
| | | // Don't set a background image for videos - it will be handled by the video element |
| | | } elseif (isset($attrs['backgroundUrl'])) { |
| | | $styles[] = 'background-image: url('.$attrs['backgroundUrl'].')'; |
| | | return 'background-image: url('.$attrs['backgroundUrl'].')'; |
| | | } |
| | | break; |
| | | |
| | | case 'backgroundColor': |
| | | case 'borderColor': |
| | | case 'textColor': |
| | | $type = ($key === 'backgroundColor') ? 'background-color:' : (($key === 'borderColor') ? 'border-color:' : 'color:'); |
| | | $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']); |
| | | $continue = true; |
| | | foreach ($defaults as $default) { |
| | | if (str_starts_with($value, $default)) { |
| | | $continue = false; |
| | | $styles[] = $type.'var(--'.$value.')'; |
| | | } |
| | | $type = str_replace('Color', '-color', $key); |
| | | $type = str_replace('text-', '', $type); |
| | | if (isset($attrs['border']['width']) && $key === 'borderColor') { |
| | | break; |
| | | } |
| | | if ($continue) { |
| | | $styles[] = $type.$value; |
| | | } |
| | | break; |
| | | return $this->getColorStyle($value, $type); |
| | | |
| | | // Any other attributes that need direct styling |
| | | default: |
| | | $ignore = [ |
| | | 'useFeaturedImage', |
| | | 'opacity', |
| | | 'textAlign', |
| | | 'minHeightUnit', |
| | | 'isDark', |
| | | 'isUserOverlayColor', |
| | | 'contentPosition', |
| | | 'sizeSlug', |
| | | 'customOverlayColor', |
| | | 'alt', |
| | | 'placeholder', |
| | | 'imageFill', |
| | | 'mediaSizeSlug', |
| | | 'isLink', |
| | | 'kind', |
| | | 'label', |
| | | 'type', |
| | | 'id', |
| | | 'url', |
| | | 'label', |
| | | 'shouldSyncIcon', |
| | | 'rel', |
| | | 'opensInNewTab', |
| | | 'title', |
| | | 'ref', |
| | | 'overlayMenu', |
| | | 'slug', |
| | | 'theme', |
| | | 'tagName', |
| | | 'level', |
| | | 'ordered', |
| | | 'area', |
| | | 'className', |
| | | 'fontSize', |
| | | 'layout', |
| | | 'align', |
| | | 'mediaId', |
| | | 'mediaLink', |
| | | 'mediaType', |
| | | 'isStackedOnMobile', |
| | | 'width', |
| | | 'height', // maybe still need? |
| | | ]; |
| | | if (!is_admin() && !in_array($key, $ignore)) { |
| | | if (JVB_TESTING && !is_admin() && !in_array($key, $this->ignore)) { |
| | | //TESTING |
| | | // jvbDump($key, 'getStyle'); |
| | | // jvbDump($attrs); |
| | | jvbDump($attrs, '[getStyle] '.$key); |
| | | } |
| | | // No default inline styles |
| | | break; |
| | |
| | | |
| | | return $styles; |
| | | } |
| | | private function getIconSizeStyle(string $value):array |
| | | { |
| | | $values = explode(' ', $value); |
| | | $styles = []; |
| | | foreach ($values as $v) { |
| | | switch ($value) { |
| | | case 'has-small-icon-size': |
| | | $styles[] = '--w:var(--txt-x-small)'; |
| | | break; |
| | | case 'has-large-icon-size': |
| | | $styles[] = '--w:var(--txt-large)'; |
| | | break; |
| | | case 'has-huge-icon-size': |
| | | $styles[] = '--w:var(--txt-xx-large)'; |
| | | break; |
| | | default: |
| | | if (JVB_TESTING) { |
| | | jvbDump($value, 'No preset found for size: '.print_r($value, true)); |
| | | } |
| | | } |
| | | } |
| | | return $styles; |
| | | } |
| | | private function getFontFamilyStyle(string $value):string |
| | | { |
| | | return match($value) { |
| | | 'body' => 'font-family: var(--body)', |
| | | 'heading' => 'font-family: var(--heading)', |
| | | default => sprintf('font-family: %s', $value) |
| | | }; |
| | | } |
| | | private function getColorStyle(string $value, ?string $type = 'color'):string |
| | | { |
| | | if (!in_array($type, ['color', 'background','background-color','border-color'])) { |
| | | $type = null; |
| | | } |
| | | return sprintf( |
| | | '%s%s', |
| | | $type ? $type.': ' : '', |
| | | $this->getColor($value) |
| | | ); |
| | | } |
| | | private function getMinHeightStyle(string $value, array $attrs):string |
| | | { |
| | | $out = ''; |
| | | if (!empty($value)) { |
| | | if (isset($attrs['minHeightUnit'])) { |
| | | $out = sprintf( |
| | | 'min-height: %s%s', |
| | | $value, |
| | | $attrs['minHeightUnit'] |
| | | ); |
| | | } else { |
| | | $out = sprintf( |
| | | 'min-height: %spx', |
| | | $value |
| | | ); |
| | | } |
| | | } |
| | | return $out; |
| | | } |
| | | |
| | | private function getFocalPointStyle(array $value):string |
| | | { |
| | | jvbDump($value, 'Focal Point'); |
| | | $x = array_key_exists('x', $value) ? $value['x'] * 100 : 'center'; |
| | | $y = array_key_exists('y', $value) ? $value['y'] * 100 : 'center'; |
| | | |
| | | $y = $x === $y ? '' : ' '.$y; |
| | | return sprintf( |
| | | 'background-position:%s%s', |
| | | $x, |
| | | $y |
| | | ); |
| | | } |
| | | |
| | | private function extractStyles(array $value, array $attrs):array |
| | | { |
| | | $styles = []; |
| | | foreach ($value as $k => $v) { |
| | | switch ($k) { |
| | | case 'border': |
| | | $styles = array_merge($styles, $this->getBorderStyle($v, $attrs)); |
| | | break; |
| | | |
| | | case 'color': |
| | | if (isset($v['background'])) { |
| | | $styles[] = $this->getColorStyle($v['background'], 'background-color'); |
| | | } |
| | | if (isset($v['text'])) { |
| | | $styles[] = $this->getColorStyle($v['text']); |
| | | } |
| | | if (isset($v['gradient'])) { |
| | | jvbDump($v, 'Gradient'); |
| | | } |
| | | break; |
| | | |
| | | case 'layout': |
| | | $styles = array_merge($styles, $this->getLayoutStyle($v, $attrs)); |
| | | break; |
| | | |
| | | case 'typography': |
| | | $styles = array_merge($styles, $this->getTypographyStyle($v, $attrs)); |
| | | break; |
| | | |
| | | case 'spacing': |
| | | if (isset($v['blockGap'])) { |
| | | if (is_array($v['blockGap'])) { |
| | | $inner = []; |
| | | foreach ($v['blockGap'] as $gap) { |
| | | $inner[] = sprintf( |
| | | 'var(--sp%s)', |
| | | $this->getPresetSpacing($gap) |
| | | ); |
| | | } |
| | | if (!empty($inner)) { |
| | | $styles[] = sprintf( |
| | | '--gap: %s', |
| | | implode(' ', $inner) |
| | | ); |
| | | } |
| | | } else { |
| | | $styles[] = '--gap: var(--sp'.$this->getPresetSpacing($v['blockGap']).')'; |
| | | } |
| | | } |
| | | |
| | | // Don't duplicate margin/padding that's handled by classes |
| | | // Only add specific CSS values here that wouldn't work well as classes |
| | | if (isset($v['margin'])) { |
| | | foreach ($v['margin'] as $direction => $size) { |
| | | if (!str_contains($size, 'var:preset')) { |
| | | $styles[] = 'margin-'.$direction.': '.$size; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (isset($v['padding'])) { |
| | | foreach($v['padding'] as $dir => $size) { |
| | | if (!str_contains($size, 'var:preset')) { |
| | | $styles[] = 'padding-'.$dir.': '.$size; |
| | | } |
| | | } |
| | | } |
| | | break; |
| | | |
| | | case 'background': |
| | | if (array_key_exists('backgroundImage', $v)) { |
| | | $data = Image::getData($v['backgroundImage']['id']); |
| | | if (!empty($data) && array_key_exists('tiny', $data)) { |
| | | $styles[] = sprintf( |
| | | 'background-image: url(%s)', |
| | | $data['tiny'] |
| | | ); |
| | | } |
| | | |
| | | } |
| | | break; |
| | | |
| | | case 'dimensions': |
| | | foreach ($v as $sk => $sv) { |
| | | if ($sk === 'minHeight') { |
| | | $styles[] = sprintf( |
| | | 'min-height: %s', |
| | | $sv |
| | | ); |
| | | } else { |
| | | jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true)); |
| | | } |
| | | } |
| | | |
| | | |
| | | break; |
| | | |
| | | case 'elements': |
| | | if (!empty($v)) { |
| | | // Generate a unique class tied to this block instance |
| | | $uid = 'b-'.substr(md5(serialize($attrs)), 0, 8); |
| | | $this->extractElementStyles($v, $uid); |
| | | // We need the uid added as a class — store it for getClassesAndStyles to pick up |
| | | static::$pendingClass[] = $uid; |
| | | } |
| | | break; |
| | | |
| | | default: |
| | | jvbDump('No config set for '.$k.': '.print_r($v, true)); |
| | | } |
| | | } |
| | | |
| | | return $styles; |
| | | } |
| | | private function getBorderStyle(array $border, array $attrs):array |
| | | { |
| | | $styles = []; |
| | | |
| | | if (isset($border['radius'])) { |
| | | $styles[] = 'border-radius: '.$border['radius']; |
| | | } |
| | | |
| | | if (isset($border['width']) && isset($attrs['borderColor'])) { |
| | | $st = $border['style'] ?? 'solid'; |
| | | $styles[] = sprintf( |
| | | 'border: %s %s %s', |
| | | $border['width'], |
| | | $st, |
| | | $this->getColor($attrs['borderColor']) |
| | | ); |
| | | } elseif (isset($border['width'])) { |
| | | $styles[] = 'border-width: '.$border['width']; |
| | | } elseif (isset($border['style'])) { |
| | | $styles[] = 'border-style: '.$border['style']; |
| | | } |
| | | |
| | | return $styles; |
| | | } |
| | | |
| | | private function getLayoutStyle(array $layout, array $attrs):array |
| | | { |
| | | $styles = []; |
| | | // jvbDump($layout); |
| | | |
| | | foreach ($layout as $l => $option) { |
| | | switch ($l) { |
| | | case 'selfStretch': |
| | | if ($option === 'fixed' && isset($layout['selfStretchValue'])) { |
| | | $styles[] = 'width: '.$layout['selfStretchValue']; |
| | | } elseif ($option === 'fill') { |
| | | $styles[] = 'flex:1'; |
| | | } |
| | | break; |
| | | default: |
| | | $ignore = [ |
| | | 'selfStretchValue', |
| | | 'flexSize', |
| | | ]; |
| | | if (JVB_TESTING && !in_array($l, $ignore)) { |
| | | jvbDump($l, 'No layout style set for: '); |
| | | } |
| | | // case 'type': |
| | | // if ($option === 'grid' && $value['layout']['columnCount'] !== 3) { |
| | | // $styles[] = sprintf( |
| | | // 'grid-template-columns: repeat(1fr, %s)', |
| | | // $value['layout']['columnCount'] |
| | | // ); |
| | | // } |
| | | // break; |
| | | } |
| | | } |
| | | return $styles; |
| | | } |
| | | |
| | | private function getTypographyStyle(array $typography, array $attrs):array |
| | | { |
| | | $styles = []; |
| | | |
| | | if (isset($typography['fontSize'])) { |
| | | $styles[] = 'font-size: '.$typography['fontSize']; |
| | | } |
| | | |
| | | if (isset($typography['fontWeight'])) { |
| | | $styles[] = 'font-weight: '.$typography['fontWeight']; |
| | | } |
| | | |
| | | if (isset($typography['textDecoration'])) { |
| | | $styles[] = 'text-decoration: '.$typography['textDecoration']; |
| | | } |
| | | |
| | | if (isset($typography['textTransform'])) { |
| | | $styles[] = 'text-transform: '.$typography['textTransform']; |
| | | } |
| | | |
| | | if (isset($typography['letterSpacing'])) { |
| | | $styles[] = 'letter-spacing: '.$typography['letterSpacing']; |
| | | } |
| | | |
| | | if (isset($typography['lineHeight'])) { |
| | | $styles[] = 'line-height: '.$typography['lineHeight']; |
| | | } |
| | | return $styles; |
| | | } |
| | | private function extractElementStyles(array $elements, string $uid):void |
| | | { |
| | | foreach ($elements as $element => $states) { |
| | | $selector = match($element) { |
| | | 'link' => "a", |
| | | 'heading' => "h1,h2,h3,h4,h5,h6", |
| | | 'button' => "button,.button", |
| | | default => $element, |
| | | }; |
| | | |
| | | foreach ($states as $state => $rules) { |
| | | $css = []; |
| | | $fullSelector = str_starts_with($state, ':') |
| | | ? ".{$uid} {$selector}{$state}" |
| | | : ".{$uid} {$selector}"; |
| | | |
| | | if (isset($rules['color']['text'])) { |
| | | $css[] = 'color: '.$this->getColor($rules['color']['text']); |
| | | } |
| | | if (isset($rules['color']['background'])) { |
| | | $css[] = 'background-color: '.$this->getColor($rules['color']['background']); |
| | | } |
| | | if (!empty($css)) { |
| | | static::$pendingStyles[] = $fullSelector.' { '.implode('; ', $css).' }'; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | public function maybeOutputCustomStyles(): string |
| | | { |
| | | if (empty(static::$pendingStyles)) return ''; |
| | | $out = '<style>'.implode(' ', static::$pendingStyles).'</style>'; |
| | | static::$pendingStyles = []; |
| | | return $out; |
| | | } |
| | | |
| | | private function getDimRatioStyle(int $value, array $attrs):string |
| | | { |
| | | //TODO: This likely isn't working correctly |
| | | jvbDump($value, 'dimRatio'); |
| | | jvbDump($attrs, 'dimRatio attrs'); |
| | | $s = ''; |
| | | if (!array_key_exists('overlayColor', $attrs)) { |
| | | $s = 'background-color: rgba(var(--base-rgb), '; |
| | | if ($value <= 14) { |
| | | $s .= 'var(--op-1));'; |
| | | } elseif ($value <= 28) { |
| | | $s .= 'var(--op-2));'; |
| | | } elseif ($value <= 42) { |
| | | $s .= 'var(--op-3));'; |
| | | } elseif ($value <= 56) { |
| | | $s .= 'var(--op-45));'; |
| | | } elseif ($value <= 70) { |
| | | $s .= 'var(--op-4));'; |
| | | } elseif ($value <= 84) { |
| | | $s .= 'var(--op-5));'; |
| | | } else { |
| | | $s .= 'var(--op-6));'; |
| | | } |
| | | } |
| | | return $s; |
| | | } |
| | | |
| | | protected function getDataset(array $attrs):array |
| | | { |
| | | $dataset = []; |
| | | if (array_key_exists('style', $attrs)) { |
| | | if (array_key_exists('background', $attrs['style'])){ |
| | | if (array_key_exists('backgroundImage', $attrs['style']['background'])) { |
| | | $id = $attrs['style']['background']['backgroundImage']['id']??false; |
| | | if ($id) { |
| | | $data = Image::getData($id); |
| | | $dataset['bg-small'] = $data['small']; |
| | | $dataset['bg-med'] = $data['medium']; |
| | | $dataset['bg-large'] = $data['large']; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return $dataset; |
| | | } |
| | | |
| | | public function formatImage(int $ID = 0, string $start = 'tiny', string $replace = 'large'):string |
| | | { |
| | |
| | | return jvbFormatImage($ID, $start, $replace); |
| | | } |
| | | |
| | | protected function checkAttrs(string $test, array $attrs):bool |
| | | { |
| | | return array_key_exists($test, $attrs) && $attrs[$test]===true; |
| | | } |
| | | |
| | | protected function getColor(string $value):string |
| | | { |
| | | $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']); |
| | | foreach ($defaults as $default) { |
| | | if (str_starts_with($value, $default)) { |
| | | return 'var(--'.$value.')'; |
| | | } |
| | | } |
| | | return $value; |
| | | } |
| | | |
| | | protected function counter(string $key):void |
| | | { |
| | | if (!array_key_exists($key, static::$counters)) { |
| | | static::$counters[$key] = 1; |
| | | } else { |
| | | static::$counters[$key]++; |
| | | } |
| | | } |
| | | } |
| | |
| | | <?php if ($hasMany) { |
| | | //If we have multiple content, only show the content first |
| | | ?> |
| | | <details class="col a-start"> |
| | | <summary class="row btw"> |
| | | <details class="col left"> |
| | | <summary class="row x-btw"> |
| | | <span class="label">SHOWING: </span> |
| | | <?php |
| | | $labels = []; |
| | |
| | | if (!empty ($this->config['taxonomies'])) { |
| | | ?> |
| | | <div class="filters"> |
| | | <div class="filter-group row start"> |
| | | <div class="filter-group row left"> |
| | | <span class="label">FILTER BY:</span> |
| | | |
| | | <?php |
| | |
| | | </div> |
| | | </div> |
| | | <?php } ?> |
| | | <div class="row btw nowrap"> |
| | | <div class="order-by filter-group row start w-full"> |
| | | <div class="row x-btw nowrap"> |
| | | <div class="order-by filter-group row left w-full"> |
| | | <span class="label">ORDER BY:</span> |
| | | <?php |
| | | //TODO: Get content types that can be sorted alphabetically |
| | |
| | | |
| | | </div> |
| | | |
| | | <div class="order-direction filter-group row start w-full" data-for-order="date,modified,title<?= $custom === '' ? '' : ','.$custom?>"> |
| | | <div class="order-direction filter-group row left w-full" data-for-order="date,modified,title<?= $custom === '' ? '' : ','.$custom?>"> |
| | | <span class="label">ORDER:</span> |
| | | <input type="radio" id="order-desc" class="btn" name="order" value="desc" data-filter="order" checked> |
| | | <label for="order-desc" title="Sort Descending (A-Z, 1-10)" class="row"> |
| | |
| | | |
| | | // Render navigation if multiple sections |
| | | if (count($sections) > 1) { |
| | | echo '<nav class="tabs row start" role="tablist">'; |
| | | echo '<nav class="tabs row left" role="tablist">'; |
| | | $i = 1; |
| | | foreach ($sections as $slug => $section) { |
| | | $active_class = $i === 1 ? ' active' : ''; |
| | |
| | | } |
| | | |
| | | // Add step navigation buttons |
| | | echo '<div class="step-navigation row btw">'; |
| | | echo '<div class="step-navigation row x-btw">'; |
| | | |
| | | if ($i > 0) { |
| | | echo '<button type="button" class="button secondary prev-step" data-action="prev-step">'; |
| | |
| | | |
| | | ?> |
| | | <div class="menu-item<?= !empty($variations) ? ' variable' : '' ?>" data-section="<?=$slug?>"> |
| | | <div class="header row btw"> |
| | | <div class="header row x-btw"> |
| | | <h3><?= $values['post_title']?></h3> |
| | | <p class="price"><?= $priceRange ?></p> |
| | | </div> |
| | | <div class="description"> |
| | | <?= Render::renderFrom($meta, 'post_excerpt')?> |
| | | </div> |
| | | <div class="info row end"> |
| | | <div class="info row right"> |
| | | <?php |
| | | if (empty($variations)) { |
| | | Form::renderFrom($meta, |
| | |
| | | $open = $this->isOpen ? ' open' : ''; |
| | | ?> |
| | | <details class="info"<?=$open?>> |
| | | <summary class="row btw"><?= $this->detailsTitle ?></summary> |
| | | <summary class="row x-btw"><?= $this->detailsTitle ?></summary> |
| | | <?php |
| | | foreach ($this->details as $key => $details) { |
| | | if ($details === '') { |
| | |
| | | <div class="items-wrap"> |
| | | <!-- Common/Favorite terms section --> |
| | | <details class="favourite-terms" hidden> |
| | | <summary class="title row btw">Your Go Tos:</summary> |
| | | <ul class="favourite-list row btw"></ul> |
| | | <summary class="title row x-btw">Your Go Tos:</summary> |
| | | <ul class="favourite-list row x-btw"></ul> |
| | | </details> |
| | | |
| | | <!-- Pagination info --> |
| | |
| | | { <span>loading items</span> } |
| | | </p> |
| | | <!-- Terms list --> |
| | | <ul class="items-container col start" role="listbox" aria-label="Available terms"> |
| | | <ul class="items-container col top" role="listbox" aria-label="Available terms"> |
| | | <!-- Terms will be populated here --> |
| | | </ul> |
| | | |
| | |
| | | |
| | | <!-- Create new term section --> |
| | | <details class="create-term" hidden> |
| | | <summary class="row btw">Add New Term</summary> |
| | | <summary class="row x-btw">Add New Term</summary> |
| | | <div class="create-new-term-section"> |
| | | <form class="create-term" data-nocache data-form-id="create-term" data-save="terms"> |
| | | <div class="form-group"> |
| | |
| | | ?> |
| | | <div class="jvb-selector <?= esc_attr($this->name) ?>" |
| | | id="<?= esc_attr($this->id) ?>"<?= $hidden ?>> |
| | | <div class="field-group-header row btw"> |
| | | <div class="field-group-header row x-btw"> |
| | | <label for="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete"> |
| | | <?= ($this->config['icon']) ? jvbIcon($this->config['icon']) : '' ?> |
| | | <span><?= $this->config['label'] ?></span> |
| | |
| | | } |
| | | |
| | | echo '<div class="container">'; |
| | | $nav = '<nav class="tabs row start" role="tablist">'; |
| | | $nav = '<nav class="tabs row left" role="tablist">'; |
| | | $i = 1; |
| | | foreach ($registrar->getSections() as $slug => $section) { |
| | | $nav .= '<button type="button" class="tab'; |
| | |
| | | <img src="" alt="" class="image"> |
| | | <img src="" alt="" class="image-right"> |
| | | <details> |
| | | <summary class="row btw">DETAILS</summary> |
| | | <summary class="row x-btw">DETAILS</summary> |
| | | <div class="item-info"></div> |
| | | </details> |
| | | </div> |
| | |
| | | $date = new Date('M j, Y', strtotime($review['date'])); |
| | | } |
| | | |
| | | $out .= '<cite class="row btw">'; |
| | | $out .= '<cite class="row x-btw">'; |
| | | if ($review['rating'] !== 'none') { |
| | | $out .= jvbFormatStarRating($review['rating']); |
| | | } |
| | |
| | | </div> |
| | | </div> |
| | | <details> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | Extra Fields |
| | | </summary> |
| | | <div class="fields"></div> |
| | |
| | | } |
| | | |
| | | ?> |
| | | <aside id="queue" class="left col start btw main" aria-expanded="false" > |
| | | <div class="m-actions row start nowrap"> |
| | | <aside id="queue" class="left col top main" aria-expanded="false" > |
| | | <div class="m-actions row left nowrap"> |
| | | <button class="refresh row" title="Check now"> |
| | | <?= jvbIcon('arrows-clockwise', ['title'=> 'Check now']) ?> |
| | | <span class="countdown row indicator" title="Will refresh again...">5</span> |
| | |
| | | <div class="popup row"><span></span></div> |
| | | </div> |
| | | |
| | | <div class="header col start"> |
| | | <div class="header col top"> |
| | | <h2>Queue Status</h2> |
| | | <nav class="filters"> |
| | | <?php |
| | |
| | | ?> |
| | | </nav> |
| | | </div> |
| | | <div class="qitems item-grid col a-start nowrap"> |
| | | <div class="qitems item-grid col left nowrap"> |
| | | </div> |
| | | <div class="queue-actions row btw nowrap"> |
| | | <div class="queue-actions row x-btw nowrap"> |
| | | <button class="dismiss-all">Clear Completed</button> |
| | | <button class="retry-all">Retry Failed</button> |
| | | </div> |
| | |
| | | </button> |
| | | <template class="queueItem"> |
| | | <div class="item"> |
| | | <div class="header row btw"> |
| | | <div class="header row x-btw"> |
| | | <span class="type"></span> |
| | | <span class="status row"><?= jvbIcon('arrows-clockwise') ?><span class="screen-reader-text"></span></span> |
| | | </div> |
| | | <?php jvbRenderProgressBar('',false,false) ?> |
| | | <div class="info"> |
| | | <div class="details"></div> |
| | | <div class="time row start"> |
| | | <div class="time row left"> |
| | | <?= jvbIcon('clock') ?> |
| | | <span class="started">Started: <time></time></span> |
| | | <span class="completed" hidden><span>Completed: </span><time></time></span> |
| | | </div> |
| | | </div> |
| | | <div class="actions row end"> |
| | | <div class="actions row right"> |
| | | <button class="retry" data-action="retry"><span>Retry</span><?= jvbIcon('arrows-clockwise')?></button> |
| | | <button class="cancel" data-action="cancel"><span>Cancel</span><?= jvbIcon('x-square')?></button> |
| | | <button class="refresh" data-action="refresh" title="Refresh to see changes"><span>Refresh</span><?= jvbIcon('arrows-clockwise')?></button> |
| | |
| | | * Outputs the search bar (likely don't need anymore) |
| | | * @return string |
| | | */ |
| | | function jvbSearch(string $placeholder = 'Search...', string $id = 'search'):string |
| | | function jvbSearch(string $placeholder = 'Search...', string $id = 'search', string $label = '', string $buttonText = '', bool $buttonInside = false, bool $hideSearch = false):string |
| | | { |
| | | return Form::search($placeholder, $id); |
| | | return Form::search($placeholder, $id, $label, $buttonText, $buttonInside, $hideSearch); |
| | | } |
| | | |
| | | |
| | |
| | | </aside> |
| | | <template class="notificationPopup"> |
| | | <div class="toast" role="status" aria-live="polite"> |
| | | <div class="toast-content row btw"> |
| | | <div class="toast-content row x-btw"> |
| | | <p></p> |
| | | <button type="button" class="close-toast" aria-label="Close"> |
| | | <?= jvbIcon('x') ?> |
| | |
| | | echo ''; |
| | | return ''; |
| | | } |
| | | $header = '<nav class="tabs row start" role="tablist">'; |
| | | $header = '<nav class="tabs row left" role="tablist">'; |
| | | $content = ''; |
| | | $i = 0; |
| | | |
| | |
| | | <div class="bar"> |
| | | <div class="fill"></div> |
| | | </div> |
| | | <div class="row btw"> |
| | | <div class="row x-btw"> |
| | | %s |
| | | <div class="details"> |
| | | %s |
| | |
| | | ?> |
| | | <form id="<?=$this->service_name?>" class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>" |
| | | data-service="<?php echo esc_attr($this->service_name); ?>"> |
| | | <div class="header row btw"> |
| | | <div class="header row x-btw"> |
| | | <h3><?php echo esc_html($this->title); ?></h3> |
| | | <div class="setup"> |
| | | <?php if ($is_connected): ?> |
| | |
| | | } |
| | | ?> |
| | | </div> |
| | | <div class="actions row btw wrap"> |
| | | <div class="actions row x-btw wrap"> |
| | | <?php |
| | | foreach ($this->buttons as $action => $label) { |
| | | if (!$is_connected && $action !== 'save_credentials') { |
| | |
| | | |
| | | <?php |
| | | $menu = new Navigation('sidebar'); |
| | | $menuClasses = ['col', 'a-start', 'nowrap']; |
| | | $menuClasses = ['col', 'left', 'nowrap']; |
| | | $itemClasses = ['col']; |
| | | $menu->addClass('col a-start')->hasToggle()->defaultMenuClasses($menuClasses); |
| | | $menu->addClass('col left')->hasToggle()->defaultMenuClasses($menuClasses); |
| | | $menu->defaultItemClasses($itemClasses); |
| | | $pages = $this->getUserAllowedPages()?:[]; |
| | | //Dashboard |
| | |
| | | { |
| | | ?> |
| | | <div class="approvals container"> |
| | | <nav class="tabs row start" role="tablist"> |
| | | <nav class="tabs row left" role="tablist"> |
| | | <button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true"> |
| | | <?= jvbDashIcon('infinity')?>All |
| | | </button> |
| | |
| | | } |
| | | ob_start(); |
| | | ?> |
| | | <nav class="tabs row start" role="tablist"> |
| | | <nav class="tabs row left" role="tablist"> |
| | | <?php |
| | | $i=1; |
| | | $content = Registrar::getRegistered('post'); |
| | |
| | | $config = Registrar::getInstance($slug); |
| | | $aOpen = '<a href="'.$directory['url'].'" title="See our list of '.$directory['title'].'">'; |
| | | $aClose = '</a>'; |
| | | $cache .= '<li class="directory col start"> |
| | | $cache .= '<li class="directory col left"> |
| | | '. $aOpen.jvbIcon($config->getIcon() !== '' ? $config->getIcon() :'list-dashes').$directory['title'].$aClose; |
| | | if (!empty($directory['description'])) { |
| | | $cache .= '<div class="description">'; |
| | |
| | | ); |
| | | if ($current !== '' && array_key_exists($current, $this->directories())) { |
| | | $open = ($open) ? ' open' : ''; |
| | | $cache = '<details'.$open.'><summary class="row btw">Other '.$this->referAs(true).':</summary>'. |
| | | $cache = '<details'.$open.'><summary class="row x-btw">Other '.$this->referAs(true).':</summary>'. |
| | | str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache) |
| | | .'</details>'; |
| | | } |
| | |
| | | $children =$this->renderListChunk($taxonomy, $term->term_id); |
| | | $out .= '<li>'; |
| | | if ($children !== '') { |
| | | $out .= '<details class="term"><summary class="row btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.html_entity_decode($term->name).'">'.$term->name.'</a></summary>'; |
| | | $out .= '<details class="term"><summary class="row x-btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.html_entity_decode($term->name).'">'.$term->name.'</a></summary>'; |
| | | $out .= $children; |
| | | $out .= '</details>'; |
| | | } else { |
| | |
| | | |
| | | $out = '<ul class="list-none">'; |
| | | foreach ($list as $letter => $items) { |
| | | $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>'; |
| | | $out .= '<li id="starts-with-'.$letter.'" class="row top x-btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>'; |
| | | foreach ($items as $item) { |
| | | $extra = ''; |
| | | if (!empty($item['extra'])) { |
| | |
| | | $item_html = apply_filters('jvb_directory_render_item', '', $item, $type, $extra); |
| | | |
| | | if (empty($item_html)) { |
| | | $item_html = '<li class="row btw"> |
| | | $item_html = '<li class="row x-btw"> |
| | | <a href="'.$item['url'].'" title="More about '.$item['name'].'"> |
| | | '.$item['name'].'</a>'.$extra.' |
| | | </li>'; |
| | |
| | | /** |
| | | * Get raw SVG content for CSS mask-image |
| | | */ |
| | | protected function getRawSvg(string $name, ?string $style = null): ?string |
| | | public function getRawSvg(string $name, ?string $style = null): ?string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | |
| | | $css .= 'details.all-filters summary::after,'; |
| | | } elseif ($icon === 'link') { |
| | | $css .= 'input[type=url],'; |
| | | } elseif ($icon === apply_filters('jvbSeparatorLogo', 'logo')) { |
| | | $css .= 'hr.logo::before,'; |
| | | } |
| | | $css .= ".icon-{$icon}{$styleClass}{"; |
| | | $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');"; |
| | |
| | | { |
| | | $form = $this->action.'form'; |
| | | ?> |
| | | <section class="login-box col btw"> |
| | | <section class="login-box col y-btw"> |
| | | <h1><?=$this->labels['title']?></h1> |
| | | <?= $this->labels['description'] ?> |
| | | |
| | |
| | | } |
| | | ?> |
| | | |
| | | <div class="options row btw"> |
| | | <div class="options row x-btw"> |
| | | <?php |
| | | switch ($this->action) { |
| | | case 'login': ?> |
| | |
| | | |
| | | </div> |
| | | </section> |
| | | <div class="navigation row btw"> |
| | | <div class="navigation row x-btw"> |
| | | <a href="<?= get_home_url() ?>">Home</a> |
| | | <?php |
| | | $privacy = get_privacy_policy_url(); |
| | |
| | | %s |
| | | %s |
| | | %s |
| | | <div class="row btw nowrap"> |
| | | <div class="row x-btw nowrap"> |
| | | <button type="submit" class="button button-primary button-large">%s</button> |
| | | %s |
| | | </div> |
| | |
| | | |
| | | <section class="copy"> |
| | | <h4>Your Referral Link</h4> |
| | | <div class="copy-group row btw nowrap"> |
| | | <div class="copy-group row x-btw nowrap"> |
| | | <code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link"> |
| | | <?= jvbIcon('copy'); ?> |
| | |
| | | |
| | | |
| | | <h4>Your Code</h4> |
| | | <div class="copy-group row btw nowrap"> |
| | | <div class="copy-group row x-btw nowrap"> |
| | | <code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code"> |
| | | <?= jvbIcon('copy'); ?> |
| | |
| | | </section> |
| | | |
| | | <section class="stats-summary"> |
| | | <div class="row btw"> |
| | | <div class="row x-btw"> |
| | | <span class="stat-label">Total Referrals</span> |
| | | <span class="stat-value" data-stat="total">-</span> |
| | | </div> |
| | | <div class="row btw"> |
| | | <div class="row x-btw"> |
| | | <span class="stat-label">Successful</span> |
| | | <span class="stat-value" data-stat="treated">-</span> |
| | | </div> |
| | | <div class="row btw"> |
| | | <div class="row x-btw"> |
| | | <span class="stat-label">Pending</span> |
| | | <span class="stat-value" data-stat="pending">-</span> |
| | | </div> |
| | | <div class="row btw highlight"> |
| | | <div class="row x-btw highlight"> |
| | | <span class="stat-label">Available Rewards</span> |
| | | <span class="stat-value" data-stat="rewards">$0.00</span> |
| | | </div> |
| | |
| | | <details open> |
| | | <summary>Your Code</summary> |
| | | <h3>Share Link</h3> |
| | | <div class="row btw nowrap"> |
| | | <div class="row x-btw nowrap"> |
| | | <code id="referral-link" class="copy-target"><?= home_url('/?ref=' . $referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link"> |
| | | <?= jvbIcon('copy'); ?> |
| | |
| | | </button> |
| | | </div> |
| | | <h3>Share Code</h3> |
| | | <div class="row btw nowrap"> |
| | | <div class="row x-btw nowrap"> |
| | | <code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code> |
| | | <button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code"> |
| | | <?= jvbIcon('copy'); ?> |
| | |
| | | private function __construct() |
| | | { |
| | | $this->cache = Cache::for('breadcrumbs', MONTH_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); |
| | | $this->cache->flush(); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | |
| | | return []; |
| | | } |
| | | |
| | | |
| | | switch (true) { |
| | | |
| | | case is_singular(): |
| | | $key = get_queried_object_id(); |
| | | break; |
| | |
| | | $obj = get_queried_object(); |
| | | $key = $obj->taxonomy; |
| | | break; |
| | | case is_home(): |
| | | $obj = get_queried_object(); |
| | | $key = $obj->post_type; |
| | | break; |
| | | default: |
| | | $key = 'home'; |
| | | break; |
| | |
| | | $obj = get_queried_object(); |
| | | if (is_tax()) { |
| | | $crumbs = $this->addTaxonomyCrumbs($crumbs, $obj); |
| | | } elseif (is_singular()) { |
| | | } elseif (is_singular() || is_home()) { |
| | | $crumbs = $this->addArchiveCrumbs($crumbs, $obj); |
| | | $hierarchy = $this->addSingularCrumbs($crumbs, $obj); |
| | | $crumbs = $crumbs + $hierarchy; |
| | |
| | | */ |
| | | private function addArchiveCrumbs(array $crumbs, object $obj): array |
| | | { |
| | | if (is_singular('page') || is_home()) { |
| | | return $crumbs; |
| | | } |
| | | $type = is_singular() ? $obj->post_type : $obj->name; |
| | | $name = jvbNoBase($type); |
| | | |
| | |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } else { |
| | | $postTypeObject = get_post_type_object($type); |
| | | $crumbs[] = [ |
| | | 'name' => $obj->label, |
| | | 'name' => $postTypeObject->label, |
| | | 'url' => get_post_type_archive_link($type) |
| | | ]; |
| | | } |
| | |
| | | } |
| | | } |
| | | } |
| | | $isUpdate = $meta->get('is_update'); |
| | | if (!(bool) $isUpdate) { |
| | | $meta->set('number', $index); |
| | | } |
| | | |
| | | if ($lastKey === $index) { |
| | | $latestTimestamp = strtotime($post->post_date); |
| | |
| | | if (!array_key_exists('class', $config)) { |
| | | $config['class'] = []; |
| | | } |
| | | $config['class'][] ='row btw'; |
| | | $config['class'][] ='row x-btw'; |
| | | |
| | | $checked = filter_var($value, FILTER_VALIDATE_BOOLEAN); |
| | | |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="selection-actions row btw" hidden> |
| | | <div class="selection-actions row x-btw" hidden> |
| | | <button type="button" data-action="add-to-group"> |
| | | %sGroup |
| | | </button> |
| | |
| | | protected static function renderUploadItemActions(?int $attachmentId = null):string |
| | | { |
| | | return sprintf( |
| | | '<div class="item-actions row btw"> |
| | | '<div class="item-actions row x-btw"> |
| | | <div class="btn"> |
| | | <input type="radio" class="featured btn" name="featured" id="featured%s" hidden> |
| | | <label for="featured"> |
| | |
| | | $containerId = sprintf('%s-%s-selector', $name, $config['subtype']); |
| | | |
| | | $input = sprintf( |
| | | '<div class="row btw"> |
| | | '<div class="row x-btw"> |
| | | <input type="hidden" name="%s" value="%s"> |
| | | <label for="%s-autocomplete">%s<span>%s</span></label>', |
| | | esc_attr($name), |
| | |
| | | } |
| | | $plural = static::getPlural($config); |
| | | $input .= sprintf( |
| | | '<div class="selected-items row start" role="region" aria-label="Selected %s"></div>', |
| | | '<div class="selected-items row left" role="region" aria-label="Selected %s"></div>', |
| | | $plural[1]??'' |
| | | ); |
| | | |
| | |
| | | $config['data']['tag-format'] = esc_attr($tagFormat); |
| | | |
| | | $input = sprintf( |
| | | '<h3>%s</h3><div class="row start wrap">', |
| | | '<h3>%s</h3><div class="row left wrap">', |
| | | esc_html($config['label']??'') |
| | | ); |
| | | |
| | |
| | | %s |
| | | </button> |
| | | <details%s> |
| | | <summary class="row btw repeater-row-header"> |
| | | <summary class="row x-btw repeater-row-header"> |
| | | <span class="drag-handle">%s</span> |
| | | <span class="row-number">#%s</span> |
| | | <span class="row-title">%s</span> |
| | |
| | | <div class="items-wrap"> |
| | | <!-- Common/Favorite terms section --> |
| | | <details class="favourite-terms" hidden> |
| | | <summary class="title row btw">Your Go Tos:</summary> |
| | | <ul class="favourite-list row btw"></ul> |
| | | <summary class="title row x-btw">Your Go Tos:</summary> |
| | | <ul class="favourite-list row x-btw"></ul> |
| | | </details> |
| | | |
| | | <!-- Pagination info --> |
| | |
| | | { <span>loading items</span> } |
| | | </p> |
| | | <!-- Terms list --> |
| | | <ul class="items-container col start" role="listbox" aria-label="Available terms"> |
| | | <ul class="items-container col top" role="listbox" aria-label="Available terms"> |
| | | <!-- Terms will be populated here --> |
| | | </ul> |
| | | |
| | |
| | | |
| | | <!-- Create new term section --> |
| | | <details class="create-term" hidden> |
| | | <summary class="row btw">Add New Term</summary> |
| | | <summary class="row x-btw">Add New Term</summary> |
| | | <div class="create-new-term-section"> |
| | | <form class="create-term" data-nocache data-form-id="create-term" data-save="terms"> |
| | | <div class="form-group"> |
| | |
| | | ); |
| | | } |
| | | |
| | | public static function search(string $placeholder = 'Search...', string $id = 'search'):string |
| | | public static function search(string $placeholder = 'Search...', string $id = 'search', string $label = '', string $buttonText = '',bool $buttonInside = false, bool $hideSearch = false):string |
| | | { |
| | | $id = sanitize_title($id); |
| | | $label = empty($label) ? '' : sprintf( |
| | | '<h3>%s</h3>', |
| | | $label |
| | | ); |
| | | $buttonText = empty($buttonText) ? '' : sprintf( |
| | | '<span>%s</span>', |
| | | $buttonText |
| | | ); |
| | | $hideSearch = $hideSearch ? ' hidden' : ''; |
| | | return sprintf( |
| | | '<div class="search-container row start nowrap"> |
| | | <input type="search" id="%s" placeholder="%s"> |
| | | '%s<div class="search-container row left nowrap%s"> |
| | | <input type="search" id="%s" placeholder="%s"%s> |
| | | <button title="Clear Search" type="button" class="clear-search" aria-label="Clear search" |
| | | onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();">%s</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s%s</button> |
| | | </div>', |
| | | $label, |
| | | $buttonInside ? ' insideButton' : '', |
| | | $id, |
| | | $placeholder, |
| | | $hideSearch, |
| | | jvbIcon('x', ['title' => 'Clear Search']), |
| | | jvbIcon('magnifying-glass') |
| | | jvbIcon('magnifying-glass'), |
| | | $buttonText |
| | | ); |
| | | } |
| | | } |
| | |
| | | { |
| | | return '<template class="response"> |
| | | <details class="response" open> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <div class="header"> |
| | | |
| | | </div> |
| | |
| | | { |
| | | return '<template class="responses"> |
| | | <details class="responses"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | Comments |
| | | </summary> |
| | | </details> |
| | |
| | | <h4>'.__('From:', 'jvb').'</h4> |
| | | <div class="source-info col"> |
| | | <div class="source-day"></div> |
| | | <div class="source-hours row start"></div> |
| | | <div class="source-hours row left"></div> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | protected function renderUploader(): void { |
| | | ?> |
| | | <details open class="uploader"> |
| | | <summary class="row btw"><?= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?></summary> |
| | | <summary class="row x-btw"><?= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?></summary> |
| | | <form id="uploader" data-form-id="upload_new_<?=$this->dataType ?>"> |
| | | <?php |
| | | echo jvbFormRestore(); |
| | |
| | | return; |
| | | } |
| | | ?> |
| | | <details class="all-filters col start" data-ignore> |
| | | <details class="all-filters col top" data-ignore> |
| | | <summary>Filters</summary> |
| | | <?php |
| | | |
| | |
| | | return; |
| | | } |
| | | ?> |
| | | <div class="search row start nowrap"> |
| | | <div class="search row left nowrap"> |
| | | <span class="label">Search:</span> |
| | | <?= jvbSearch() ?> |
| | | </div> |
| | |
| | | protected function renderOrderControls():void |
| | | { |
| | | ?> |
| | | <div class="radio-options order row btw w-full"> |
| | | <div class="radio-options order row x-btw w-full"> |
| | | <?php |
| | | $order = [ |
| | | 'orderby' => [ |
| | |
| | | |
| | | foreach ($order as $o => $option) { |
| | | ?> |
| | | <div class="row start"> |
| | | <div class="row left"> |
| | | <span class="label"><?= ucfirst($o)?>:</span> |
| | | <?php |
| | | $i = 0; |
| | |
| | | return; |
| | | } |
| | | ?> |
| | | <div class="filters row start"> |
| | | <div class="filters row left"> |
| | | <span class="label">Filters:</span> |
| | | <?php |
| | | foreach ($this->filters as $key => $config) { |
| | |
| | | ob_start(); |
| | | ?> |
| | | <details class="multi-select" title="Select columns" hidden> |
| | | <summary class="row start nowrap"> |
| | | <summary class="row left nowrap"> |
| | | <?= jvbDashIcon('columns') ?> |
| | | <span class="labels">Toggle Columns</span> |
| | | </summary> |
| | |
| | | return; |
| | | } |
| | | ?> |
| | | <div class="bulk-controls row nowrap btw"> |
| | | <div class="bulk-controls row nowrap x-btw"> |
| | | <div class="bulk-select"> |
| | | <input type="checkbox" id="select-all" class="select-all"> |
| | | <label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label> |
| | |
| | | } |
| | | ob_start(); |
| | | ?> |
| | | <div class="item-actions row btw abs"> |
| | | <div class="item-actions row x-btw abs"> |
| | | <?php |
| | | foreach ($this->itemActions as $action) { |
| | | $config = $this->defaultItemActions[$action]; |
| | |
| | | <div class="item <?=esc_attr($this->dataType)?> row nowrap"> |
| | | <?= $this->renderItemSelect()?> |
| | | <?=$this->renderImage() ?> |
| | | <div class="col start w-full"> |
| | | <div class="col top w-full"> |
| | | <h3 data-field="post_title"></h3> |
| | | <p data-attr="date"></p> |
| | | <p data-field="price"></p> |
| | |
| | | <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>> |
| | | <?php |
| | | if (in_array('edit', $this->caps)) { |
| | | echo $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : ''; |
| | | echo $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : ''; |
| | | if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) { |
| | | $config['autocomplete'] = true; |
| | | } |
| | |
| | | $makeThisDetailed = (in_array($config['type'], $makeDetails)); |
| | | ?> |
| | | <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>> |
| | | <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?> |
| | | <?= $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : '' ?> |
| | | <?php |
| | | if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) { |
| | | $config['autocomplete'] = true; |
| | |
| | | $makeThisDetailed = (in_array($config['type'], $makeDetails)); |
| | | ?> |
| | | <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>> |
| | | <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?> |
| | | <?= $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : '' ?> |
| | | <?= Form::render($name, '', $config); ?> |
| | | <?= $makeThisDetailed ? '</details>' : '' ?> |
| | | </td> |
| | |
| | | protected function renderTableActions(): string { |
| | | ob_start(); |
| | | ?> |
| | | <div class="table-actions row btw nowrap"> |
| | | <div class="table-actions row x-btw nowrap"> |
| | | <?php if (count(array_intersect(['create', 'edit'], $this->caps)) > 0) { ?> |
| | | <?= jvbRenderToggleTextField( |
| | | 'vertical', |
| | |
| | | |
| | | $form .= jvbRenderTabs($tabs, true); |
| | | |
| | | $form .= '<div class="cart-total row end"> |
| | | $form .= '<div class="cart-total row right"> |
| | | <p class="tax">Tax: <span></span></p> |
| | | <p class="total">GRAND TOTAL: <span></span></p> |
| | | </div> |
| | |
| | | <h3>Looks like we left things hanging</h3> |
| | | <p>We\'ve restored your cart from your last session below.</p> |
| | | <p>If you\'d rather start over, click the button below.</p> |
| | | <div class="row btw"> |
| | | <div class="row x-btw"> |
| | | <button type="button" data-clear-cart>' . jvbIcon('trash') . 'Clear Cart</button> |
| | | <button type="button" data-dismiss>' . jvbIcon('x') . 'Dismiss</button> |
| | | </div> |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $header = '<nav class="tabs row start" role="tablist">'; |
| | | $header = '<nav class="tabs row left" role="tablist">'; |
| | | $content = ''; |
| | | $i = 0; |
| | | |
| | |
| | | <div class="notification-preferences"> |
| | | <?php foreach ($favourites as $type => $items) : ?> |
| | | <details class="notification-group"> |
| | | <summary class="notification-group-header row btw"> |
| | | <summary class="notification-group-header row x-btw"> |
| | | <span class="type-label"><?=jvbIcon('heart', ['style' => 'fill'])?> <?= $notify[$type] ?></span> |
| | | <span class="type-count">( <?= count($items); ?> )</span> |
| | | </summary> |
| | |
| | | } |
| | | ); |
| | | } |
| | | |
| | | public static function getData(int $imgID):array |
| | | { |
| | | return (new Image)->getImageData($imgID); |
| | | } |
| | | |
| | | } |
| | |
| | | return IconsManager::for($source)->get($name, $options); |
| | | } |
| | | |
| | | function jvbFullIcon(string $name, array $options = []):string |
| | | { |
| | | $source = $options['source'] ?? 'icons'; |
| | | unset($options['source']); |
| | | return IconsManager::for($source)->getRawSvg($name, $options['style']??null); |
| | | } |
| | | |
| | | /** |
| | | * Get a CSS data URI for an icon |
| | | * |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/drawer-menu", |
| | | "title": "Drawer Menu", |
| | | "category": "jvb", |
| | | "icon": "menu", |
| | | "version": "1.0.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false |
| | | }, |
| | | "attributes": { |
| | | "menuId": { |
| | | "type": "string", |
| | | "default": "" |
| | | }, |
| | | "collapsed": { |
| | | "type": "boolean", |
| | | "default": true |
| | | } |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "render": "file:./render.php" |
| | | } |
| New file |
| | |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { PanelBody, ToggleControl, TextControl } from '@wordpress/components'; |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const { menuId, collapsed } = attributes; |
| | | const blockProps = useBlockProps(); |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title="Drawer Settings"> |
| | | <TextControl |
| | | label="Menu ID" |
| | | value={menuId} |
| | | onChange={(value) => setAttributes({ menuId: value })} |
| | | help="PHP-generated menu identifier" |
| | | /> |
| | | <ToggleControl |
| | | label="Start Collapsed" |
| | | checked={collapsed} |
| | | onChange={(value) => setAttributes({ collapsed: value })} |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | <div {...blockProps}> |
| | | <div className="drawer-menu-preview"> |
| | | <p>Drawer Menu: {menuId || 'Not configured'}</p> |
| | | <p>State: {collapsed ? 'Collapsed' : 'Expanded'}</p> |
| | | </div> |
| | | </div> |
| | | </> |
| | | ); |
| | | } |
| New file |
| | |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | import edit from './edit'; |
| | | import save from './save'; |
| | | import './style.scss'; |
| | | import './editor.scss'; |
| | | |
| | | registerBlockType('jvb/drawer-menu', { |
| | | edit, |
| | | save, |
| | | }); |
| New file |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\ui\Navigation; |
| | | |
| | | $menu_id = $attributes['menuId'] ?? ''; |
| | | $collapsed = $attributes['collapsed'] ?? true; |
| | | |
| | | // You'd populate this from options, a filter, or however you store menu data |
| | | $menu_items = apply_filters('jvbDrawerItems', [], $menu_id); |
| | | |
| | | if (empty($menu_items) || empty($menu_id)) { |
| | | return '<p>Please configure the drawer menu in block settings.</p>'; |
| | | } |
| | | |
| | | $cache = Cache::for('drawer'); |
| | | |
| | | if (!is_front_page()) { |
| | | $menu_items[] = [ |
| | | 'text' => 'Home', |
| | | 'url' => home_url(), |
| | | 'icon' => 'house-simple', |
| | | ]; |
| | | } |
| | | $items = array_map(function($item) { return $item['text'];}, $menu_items); |
| | | |
| | | $key = $cache->generateKey($items); |
| | | $menu = $cache->remember($key, |
| | | function () use ($menu_items, $menu_id, $collapsed) { |
| | | $menu = new Navigation($menu_id); |
| | | $menu->asDrawer($collapsed)->populateFromArray($menu_items); |
| | | return $menu->render(); |
| | | }); |
| | | |
| | | global $wp; |
| | | |
| | | $current = home_url($wp->request.'/'); |
| | | echo str_replace($current.'"', $current.'" class="current" aria-current="page"', $menu); |
| New file |
| | |
| | | export default function save() { |
| | | return null; // Server-side rendered |
| | | } |
| New file |
| | |
| | | nav.drawer { |
| | | position: fixed; |
| | | right: 0; |
| | | bottom: 0; |
| | | max-height: 100vh; |
| | | overflow: hidden auto; |
| | | width: var(--btn); |
| | | z-index: var(--z-5); |
| | | transition: var(--trans-size); |
| | | --dir: column-reverse; |
| | | background-color: var(--base); |
| | | border-left: 1px solid var(--base-200); |
| | | box-shadow:rgba(var(--base-rgb), var(--op-4)) var(--shdw-left); |
| | | height: auto; |
| | | --w: var(--chip_); |
| | | |
| | | .title, |
| | | .section-title { |
| | | display: none; |
| | | } |
| | | ul .icon { |
| | | min-width: var(--chip_); |
| | | } |
| | | |
| | | a, |
| | | .a { |
| | | height: var(--chipchip); |
| | | padding: 0 .5rem; |
| | | width: 100%; |
| | | gap: .5rem; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .toggle { |
| | | width: 100%; |
| | | .icon { |
| | | transform: rotate(0); |
| | | transition: var(--trans-transform); |
| | | } |
| | | } |
| | | &.open { |
| | | width: 240px; |
| | | .title, |
| | | .section-title { |
| | | display: block; |
| | | } |
| | | |
| | | .toggle .icon { |
| | | transform: rotate(180deg); |
| | | } |
| | | |
| | | a, |
| | | .a { |
| | | justify-content: flex-start; |
| | | } |
| | | } |
| | | |
| | | ul { |
| | | --dir: column; |
| | | --gap: 0; |
| | | --height: auto; |
| | | padding: 0; |
| | | margin: 0; |
| | | height: auto; |
| | | width:100%; |
| | | } |
| | | li { |
| | | height: auto; |
| | | width: 100%; |
| | | } |
| | | |
| | | |
| | | .row { |
| | | width: 100%; |
| | | } |
| | | |
| | | .menu-section { |
| | | border-bottom: 1px solid var(--contrast-200); |
| | | } |
| | | .section-title { |
| | | padding: 0.5rem var(--px); |
| | | font-size: var(--small); |
| | | text-transform: uppercase; |
| | | opacity: 0.6; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/faq", |
| | | "title": "FAQ Block", |
| | | "category": "jvb", |
| | | "icon": "info", |
| | | "description": "Display FAQs organized by sections with customizable ordering", |
| | | "keywords": ["faq", "questions", "help"], |
| | | "version": "1.0.0", |
| | | "textdomain": "jvb", |
| | | "attributes": { |
| | | "sectionOrder": { |
| | | "type": "array", |
| | | "default": [] |
| | | }, |
| | | "showSectionTitles": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "collapseByDefault": { |
| | | "type": "boolean", |
| | | "default": false |
| | | } |
| | | }, |
| | | "supports": { |
| | | "align": ["wide", "full"], |
| | | "html": false |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { PanelBody, ToggleControl, Notice, Button } from '@wordpress/components'; |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { useState, useEffect } from '@wordpress/element'; |
| | | import './editor.scss'; |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const { sectionOrder, showSectionTitles, collapseByDefault } = attributes; |
| | | const [sections, setSections] = useState([]); |
| | | |
| | | // Get sections from localized script |
| | | const allSections = window.jvbFaq?.sections || []; |
| | | |
| | | // Initialize sections with proper ordering |
| | | useEffect(() => { |
| | | if (!allSections.length) return; |
| | | |
| | | if (sectionOrder.length === 0) { |
| | | // First time - use default order |
| | | const orderedSections = allSections.map(section => ({ |
| | | id: section.id, |
| | | name: section.name, |
| | | })); |
| | | setSections(orderedSections); |
| | | setAttributes({ sectionOrder: orderedSections.map(s => s.id) }); |
| | | } else { |
| | | // Use saved order, add any new sections at the end |
| | | const orderedSections = []; |
| | | const existingIds = new Set(sectionOrder); |
| | | |
| | | // Add sections in saved order |
| | | sectionOrder.forEach(id => { |
| | | const section = allSections.find(s => s.id === id); |
| | | if (section) { |
| | | orderedSections.push({ id: section.id, name: section.name }); |
| | | } |
| | | }); |
| | | |
| | | // Add any new sections that weren't in the saved order |
| | | allSections.forEach(section => { |
| | | if (!existingIds.has(section.id)) { |
| | | orderedSections.push({ id: section.id, name: section.name }); |
| | | } |
| | | }); |
| | | |
| | | setSections(orderedSections); |
| | | } |
| | | }, [allSections, sectionOrder]); |
| | | |
| | | const moveSection = (index, direction) => { |
| | | const newSections = [...sections]; |
| | | const newIndex = direction === 'up' ? index - 1 : index + 1; |
| | | |
| | | if (newIndex < 0 || newIndex >= newSections.length) return; |
| | | |
| | | // Swap sections |
| | | [newSections[index], newSections[newIndex]] = [newSections[newIndex], newSections[index]]; |
| | | |
| | | setSections(newSections); |
| | | setAttributes({ sectionOrder: newSections.map(s => s.id) }); |
| | | }; |
| | | |
| | | const blockProps = useBlockProps({ |
| | | className: 'faq-block-editor', |
| | | }); |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('FAQ Settings', 'jvb')} initialOpen={true}> |
| | | <ToggleControl |
| | | label={__('Show Section Titles', 'jvb')} |
| | | checked={showSectionTitles} |
| | | onChange={(value) => setAttributes({ showSectionTitles: value })} |
| | | help={__('Display section names as headings', 'jvb')} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Collapse by Default', 'jvb')} |
| | | checked={collapseByDefault} |
| | | onChange={(value) => setAttributes({ collapseByDefault: value })} |
| | | help={__('Questions start collapsed and expand on click', 'jvb')} |
| | | /> |
| | | </PanelBody> |
| | | |
| | | <PanelBody title={__('Section Order', 'jvb')} initialOpen={false}> |
| | | <p className="components-base-control__help"> |
| | | {__('Use the arrow buttons to reorder sections', 'jvb')} |
| | | </p> |
| | | {sections.length > 0 ? ( |
| | | <div className="faq-section-list"> |
| | | {sections.map((section, index) => ( |
| | | <div key={section.id} className="faq-section-item"> |
| | | <div className="faq-section-controls"> |
| | | <Button |
| | | icon="arrow-up-alt2" |
| | | label={__('Move up', 'jvb')} |
| | | disabled={index === 0} |
| | | onClick={() => moveSection(index, 'up')} |
| | | className="faq-section-button" |
| | | /> |
| | | <Button |
| | | icon="arrow-down-alt2" |
| | | label={__('Move down', 'jvb')} |
| | | disabled={index === sections.length - 1} |
| | | onClick={() => moveSection(index, 'down')} |
| | | className="faq-section-button" |
| | | /> |
| | | </div> |
| | | <span className="faq-section-name">{section.name}</span> |
| | | </div> |
| | | ))} |
| | | </div> |
| | | ) : ( |
| | | <Notice status="info" isDismissible={false}> |
| | | {__('No sections found. Create sections in the FAQ taxonomy.', 'jvb')} |
| | | </Notice> |
| | | )} |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div {...blockProps}> |
| | | <div className="faq-block-preview"> |
| | | <h3>{__('FAQ Block', 'jvb')}</h3> |
| | | <p> |
| | | {__('This block will display FAQs organized by sections.', 'jvb')} |
| | | </p> |
| | | {sections.length > 0 ? ( |
| | | <div className="faq-sections-preview"> |
| | | <strong>{__('Section Order:', 'jvb')}</strong> |
| | | <ol> |
| | | {sections.map((section) => ( |
| | | <li key={section.id}>{section.name}</li> |
| | | ))} |
| | | </ol> |
| | | </div> |
| | | ) : ( |
| | | <Notice status="warning" isDismissible={false}> |
| | | {__('No sections available. Create sections in the FAQ taxonomy.', 'jvb')} |
| | | </Notice> |
| | | )} |
| | | </div> |
| | | </div> |
| | | </> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * FAQ Block - Editor Styles |
| | | */ |
| | | |
| | | .faq-block-editor { |
| | | padding: 2rem; |
| | | border: 2px dashed #ccc; |
| | | border-radius: 8px; |
| | | background: #f9f9f9; |
| | | |
| | | .faq-block-preview { |
| | | text-align: center; |
| | | |
| | | h3 { |
| | | margin: 0 0 0.5rem; |
| | | font-size: 1.25rem; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | > p { |
| | | margin: 0 0 1.5rem; |
| | | color: #666; |
| | | } |
| | | |
| | | .faq-sections-preview { |
| | | margin-top: 1.5rem; |
| | | text-align: left; |
| | | background: white; |
| | | padding: 1rem; |
| | | border-radius: 4px; |
| | | |
| | | strong { |
| | | display: block; |
| | | margin-bottom: 0.5rem; |
| | | } |
| | | |
| | | ol { |
| | | margin: 0; |
| | | padding-left: 1.5rem; |
| | | |
| | | li { |
| | | margin: 0.25rem 0; |
| | | padding: 0.25rem 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Inspector Controls |
| | | .faq-section-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 0.5rem; |
| | | margin-top: 0.5rem; |
| | | } |
| | | |
| | | .faq-section-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 0.5rem; |
| | | padding: 0.5rem 0.75rem; |
| | | background: white; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | transition: background 150ms ease; |
| | | |
| | | &:hover { |
| | | background: #f9f9f9; |
| | | } |
| | | } |
| | | |
| | | .faq-section-controls { |
| | | display: flex; |
| | | gap: 0.25rem; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .faq-section-button { |
| | | min-width: 30px !important; |
| | | padding: 4px !important; |
| | | height: 30px !important; |
| | | |
| | | &:disabled { |
| | | opacity: 0.3; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | .faq-section-name { |
| | | flex: 1; |
| | | font-weight: 500; |
| | | padding-left: 0.5rem; |
| | | } |
| | | |
| | | // Notice adjustments |
| | | .components-panel__body .components-notice { |
| | | margin: 1rem 0; |
| | | } |
| New file |
| | |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | import Edit from './edit'; |
| | | import './style.scss'; |
| | | import './editor.scss'; |
| | | import metadata from './block.json'; |
| | | |
| | | registerBlockType(metadata.name, { |
| | | edit: Edit, |
| | | // No save function - dynamic block rendered on server |
| | | save: () => null, |
| | | }); |
| New file |
| | |
| | | nav#faq { |
| | | height: max-content; |
| | | display: block; |
| | | background-color: var(--base-100); |
| | | border-radius: var(--radius-outer); |
| | | padding: 1.5rem; |
| | | touch-action: auto; |
| | | ol { |
| | | list-style: decimal-leading-zero; |
| | | height: fit-content; |
| | | display: block; |
| | | counter-reset: faq; |
| | | li { |
| | | counter-increment: faq; |
| | | width: max-content; |
| | | &::before { |
| | | content: counter(faq); |
| | | display: block; |
| | | font-family: var(--heading); |
| | | font-weight: var(--fw-h-bold); |
| | | } |
| | | } |
| | | } |
| | | h2 { |
| | | left: 0; |
| | | font-size: var(--txt-large); |
| | | margin: .5rem 0 .5rem; |
| | | } |
| | | a { |
| | | padding: .5rem; |
| | | } |
| | | } |
| | | |
| | | .faq-block { |
| | | padding-bottom: 3rem; |
| | | max-width: none; |
| | | width: 100%; |
| | | > * { |
| | | max-width: var(--wide); |
| | | margin: 1rem auto; |
| | | } |
| | | h2 { |
| | | margin: 5rem 0 1.5rem; |
| | | } |
| | | h3 { |
| | | margin: 0; |
| | | text-transform: none; |
| | | } |
| | | :target { |
| | | background-color: var(--base); |
| | | outline: none; |
| | | |
| | | h2 { |
| | | background-color: var(--base); |
| | | padding: 1rem 1.5rem; |
| | | border-radius: var(--radius-outer); |
| | | } |
| | | } |
| | | details { |
| | | max-width: var(--content); |
| | | margin: 1rem auto; |
| | | padding: .75rem; |
| | | } |
| | | details + details { |
| | | margin-top: 3rem; |
| | | } |
| | | details .button { |
| | | height: fit-content; |
| | | display: flex; |
| | | margin-left: auto; |
| | | } |
| | | } |
| New file |
| | |
| | | /** |
| | | * FAQ Block - Frontend Interactions |
| | | * Handles accordion functionality for FAQ items |
| | | */ |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | const faqBlocks = document.querySelectorAll('.faq-block'); |
| | | |
| | | faqBlocks.forEach((block) => { |
| | | const faqItems = block.querySelectorAll('.faq-item'); |
| | | |
| | | faqItems.forEach((item) => { |
| | | const button = item.querySelector('.faq-item__question'); |
| | | const answer = item.querySelector('.faq-item__answer'); |
| | | |
| | | if (!button || !answer) return; |
| | | |
| | | button.addEventListener('click', () => { |
| | | const isExpanded = button.getAttribute('aria-expanded') === 'true'; |
| | | |
| | | // Toggle this item |
| | | button.setAttribute('aria-expanded', !isExpanded); |
| | | |
| | | if (isExpanded) { |
| | | // Collapse |
| | | answer.style.height = answer.scrollHeight + 'px'; |
| | | // Force reflow |
| | | answer.offsetHeight; |
| | | answer.style.height = '0'; |
| | | |
| | | setTimeout(() => { |
| | | answer.style.display = 'none'; |
| | | answer.style.height = ''; |
| | | }, 300); |
| | | |
| | | item.classList.remove('faq-item--expanded'); |
| | | } else { |
| | | // Expand |
| | | answer.style.display = 'block'; |
| | | answer.style.height = '0'; |
| | | // Force reflow |
| | | answer.offsetHeight; |
| | | answer.style.height = answer.scrollHeight + 'px'; |
| | | |
| | | setTimeout(() => { |
| | | answer.style.height = 'auto'; |
| | | }, 300); |
| | | |
| | | item.classList.add('faq-item--expanded'); |
| | | } |
| | | }); |
| | | |
| | | // Handle keyboard navigation |
| | | button.addEventListener('keydown', (e) => { |
| | | // Space or Enter triggers the button |
| | | if (e.key === ' ' || e.key === 'Enter') { |
| | | e.preventDefault(); |
| | | button.click(); |
| | | } |
| | | }); |
| | | }); |
| | | }); |
| | | |
| | | // Optional: Add URL hash navigation |
| | | // If URL has #faq-123, open that specific FAQ |
| | | if (window.location.hash) { |
| | | const hash = window.location.hash.substring(1); |
| | | const targetItem = document.querySelector(`[data-faq-id="${hash}"]`); |
| | | |
| | | if (targetItem) { |
| | | const button = targetItem.querySelector('.faq-item__question'); |
| | | const isExpanded = button.getAttribute('aria-expanded') === 'true'; |
| | | |
| | | if (!isExpanded) { |
| | | button.click(); |
| | | } |
| | | |
| | | // Scroll to item after a short delay |
| | | setTimeout(() => { |
| | | targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| | | }, 350); |
| | | } |
| | | } |
| | | }); |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/feed", |
| | | "title": "Feed", |
| | | "category": "jvb", |
| | | "icon": "grid-view", |
| | | "description": "Displays a filterable feed of registered content types", |
| | | "keywords": [ "feed", "grid" ], |
| | | "version": "0.9.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "attributes": { |
| | | "title": { |
| | | "type": "string", |
| | | "default": "Your Scene" |
| | | }, |
| | | "inheritQuery": { |
| | | "type": "boolean", |
| | | "default": false |
| | | }, |
| | | "contentTypes": { |
| | | "type": "array", |
| | | "default": ["tattoo", "artwork", "artist"], |
| | | "items": { |
| | | "type": "string" |
| | | } |
| | | }, |
| | | "itemsPerPage": { |
| | | "type": "number", |
| | | "default": 36 |
| | | }, |
| | | "defaultOrder": { |
| | | "type": "string", |
| | | "default": "date_desc" |
| | | } |
| | | }, |
| | | "selectors": { |
| | | "root": ".feed-block" |
| | | }, |
| | | "styles": [ |
| | | { "name": "default", "label": "Default", "isDefault": true }, |
| | | { "name": "other", "label": "Other" } |
| | | ], |
| | | "example": { |
| | | "attributes": { |
| | | "message": "This is a notice!" |
| | | } |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * Feed Block - Edit Component |
| | | * Fetches available feed types from /jvb/v1/feed/types |
| | | * Allows configuration of content types and inherit query setting |
| | | */ |
| | | |
| | | import { useEffect, useState } from '@wordpress/element'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { |
| | | PanelBody, |
| | | CheckboxControl, |
| | | ToggleControl, |
| | | Spinner, |
| | | Notice |
| | | } from '@wordpress/components'; |
| | | import apiFetch from '@wordpress/api-fetch'; |
| | | import { __ } from '@wordpress/i18n'; |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const [feedTypes, setFeedTypes] = useState(null); |
| | | const [loading, setLoading] = useState(true); |
| | | const [error, setError] = useState(null); |
| | | |
| | | const blockProps = useBlockProps({ |
| | | className: 'feed-block-editor' |
| | | }); |
| | | |
| | | /** |
| | | * Fetch available feed types on component mount |
| | | */ |
| | | useEffect(() => { |
| | | apiFetch({ |
| | | path: '/jvb/v1/feed/types', |
| | | headers: { |
| | | 'If-Modified-Since': localStorage.getItem('feed_types_modified'), |
| | | } |
| | | }) |
| | | .then(types => { |
| | | setFeedTypes(types); |
| | | setLoading(false); |
| | | |
| | | // Store Last-Modified for future requests |
| | | // (apiFetch doesn't expose response headers easily, |
| | | // but the server will handle 304s) |
| | | |
| | | // Initialize contentTypes if not set and not inheriting |
| | | if (!attributes.contentTypes && !attributes.inheritQuery) { |
| | | const firstType = Object.keys(types)[0]; |
| | | if (firstType) { |
| | | setAttributes({ contentTypes: [firstType] }); |
| | | } |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error('Error loading feed types:', err); |
| | | setError(err.message); |
| | | setLoading(false); |
| | | }); |
| | | }, [attributes.inheritQuery]); |
| | | |
| | | /** |
| | | * Toggle a content type in the selection |
| | | */ |
| | | const toggleContentType = (slug, checked) => { |
| | | const currentTypes = attributes.contentTypes || []; |
| | | |
| | | const newTypes = checked |
| | | ? [...currentTypes, slug] |
| | | : currentTypes.filter(t => t !== slug); |
| | | |
| | | setAttributes({ contentTypes: newTypes }); |
| | | }; |
| | | |
| | | /** |
| | | * Get friendly label for content type |
| | | */ |
| | | const getTypeLabel = (slug, config) => { |
| | | return `${config.plural} (${config.type})`; |
| | | }; |
| | | |
| | | /** |
| | | * Group types by category for better UX |
| | | */ |
| | | const groupedTypes = feedTypes ? { |
| | | content: Object.entries(feedTypes) |
| | | .filter(([_, config]) => config.type === 'content'), |
| | | taxonomy: Object.entries(feedTypes) |
| | | .filter(([_, config]) => config.type === 'taxonomy') |
| | | } : { content: [], taxonomy: [] }; |
| | | |
| | | return ( |
| | | <div {...blockProps}> |
| | | <InspectorControls> |
| | | <PanelBody |
| | | title={__('Feed Settings', 'jvb')} |
| | | initialOpen={true} |
| | | > |
| | | <ToggleControl |
| | | label={__('Inherit from Page Context', 'jvb')} |
| | | help={ |
| | | attributes.inheritQuery |
| | | ? __('Feed will adapt to the current page (profile, taxonomy, etc.)', 'jvb') |
| | | : __('Manually select content types to display', 'jvb') |
| | | } |
| | | checked={attributes.inheritQuery} |
| | | onChange={(value) => setAttributes({ inheritQuery: value })} |
| | | /> |
| | | |
| | | {!attributes.inheritQuery && ( |
| | | <> |
| | | {loading && ( |
| | | <div style={{ textAlign: 'center', padding: '20px' }}> |
| | | <Spinner /> |
| | | <p>{__('Loading feed types...', 'jvb')}</p> |
| | | </div> |
| | | )} |
| | | |
| | | {error && ( |
| | | <Notice status="error" isDismissible={false}> |
| | | {__('Error loading feed types: ', 'jvb')} {error} |
| | | </Notice> |
| | | )} |
| | | |
| | | {!loading && !error && feedTypes && ( |
| | | <> |
| | | {groupedTypes.content.length > 0 && ( |
| | | <> |
| | | <h4>{__('Content Types', 'jvb')}</h4> |
| | | {groupedTypes.content.map(([slug, config]) => ( |
| | | <CheckboxControl |
| | | key={slug} |
| | | label={getTypeLabel(slug, config)} |
| | | checked={ |
| | | attributes.contentTypes?.includes(slug) || false |
| | | } |
| | | onChange={(checked) => |
| | | toggleContentType(slug, checked) |
| | | } |
| | | help={ |
| | | config.taxonomies?.length > 0 |
| | | ? `Filters: ${config.taxonomies.join(', ')}` |
| | | : null |
| | | } |
| | | /> |
| | | ))} |
| | | </> |
| | | )} |
| | | |
| | | {groupedTypes.taxonomy.length > 0 && ( |
| | | <> |
| | | <h4 style={{ marginTop: '20px' }}> |
| | | {__('Content Taxonomies', 'jvb')} |
| | | </h4> |
| | | <p style={{ fontSize: '12px', color: '#757575' }}> |
| | | {__('These are collections that group other content', 'jvb')} |
| | | </p> |
| | | {groupedTypes.taxonomy.map(([slug, config]) => ( |
| | | <CheckboxControl |
| | | key={slug} |
| | | label={getTypeLabel(slug, config)} |
| | | checked={ |
| | | attributes.contentTypes?.includes(slug) || false |
| | | } |
| | | onChange={(checked) => |
| | | toggleContentType(slug, checked) |
| | | } |
| | | help={ |
| | | config.for_content?.length > 0 |
| | | ? `Contains: ${config.for_content.join(', ')}` |
| | | : null |
| | | } |
| | | /> |
| | | ))} |
| | | </> |
| | | )} |
| | | |
| | | {!attributes.contentTypes?.length && ( |
| | | <Notice status="warning" isDismissible={false}> |
| | | {__('Please select at least one content type', 'jvb')} |
| | | </Notice> |
| | | )} |
| | | </> |
| | | )} |
| | | </> |
| | | )} |
| | | </PanelBody> |
| | | |
| | | <PanelBody |
| | | title={__('Display Settings', 'jvb')} |
| | | initialOpen={false} |
| | | > |
| | | <ToggleControl |
| | | label={__('Show Gallery View', 'jvb')} |
| | | help={__('Enable lightbox for images', 'jvb')} |
| | | checked={attributes.enableGallery || false} |
| | | onChange={(value) => |
| | | setAttributes({ enableGallery: value }) |
| | | } |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div className="feed-block-placeholder"> |
| | | <div className="feed-block-icon"> |
| | | <svg width="48" height="48" viewBox="0 0 24 24" fill="none"> |
| | | <rect x="3" y="3" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="13" y="3" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="3" y="13" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | <rect x="13" y="13" width="7" height="7" fill="currentColor" opacity="0.3" /> |
| | | </svg> |
| | | </div> |
| | | |
| | | <h3>{__('Feed Block', 'jvb')}</h3> |
| | | |
| | | {attributes.inheritQuery ? ( |
| | | <p className="feed-block-description"> |
| | | {__('📍 Inheriting from page context', 'jvb')} |
| | | </p> |
| | | ) : ( |
| | | <div className="feed-block-description"> |
| | | {attributes.contentTypes?.length > 0 ? ( |
| | | <> |
| | | <p><strong>{__('Showing:', 'jvb')}</strong></p> |
| | | <ul style={{ |
| | | listStyle: 'none', |
| | | padding: '0', |
| | | margin: '8px 0' |
| | | }}> |
| | | {attributes.contentTypes.map(type => { |
| | | const config = feedTypes?.[type]; |
| | | return ( |
| | | <li key={type} style={{ |
| | | padding: '4px 0', |
| | | color: '#2271b1' |
| | | }}> |
| | | ✓ {config?.plural || type} |
| | | </li> |
| | | ); |
| | | })} |
| | | </ul> |
| | | </> |
| | | ) : ( |
| | | <p style={{ color: '#d63638' }}> |
| | | {__('⚠️ No content types selected', 'jvb')} |
| | | </p> |
| | | )} |
| | | </div> |
| | | )} |
| | | |
| | | <p className="feed-block-note"> |
| | | {__('Feed will be displayed on the frontend', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ); |
| | | } |
| New file |
| | |
| | | .feed-content-types { |
| | | margin-bottom: 16px; |
| | | |
| | | .components-base-control__label { |
| | | margin-bottom: 8px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .components-checkbox-control { |
| | | margin-bottom: 8px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .components-checkbox-control__input-container { |
| | | margin-right: 8px; |
| | | } |
| | | } |
| | | } |
| | | .feed-block { |
| | | border: 1px solid #ddd; |
| | | padding: 20px; |
| | | background: white; |
| | | |
| | | .feed-block-preview { |
| | | .filter-preview { |
| | | display: flex; |
| | | gap: 8px; |
| | | margin: 16px 0; |
| | | flex-wrap: wrap; |
| | | |
| | | .content-type-badge { |
| | | background: #f0f0f0; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .feed-grid-placeholder { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| | | gap: 16px; |
| | | margin-top: 20px; |
| | | |
| | | .grid-item-placeholder { |
| | | background: #f0f0f0; |
| | | aspect-ratio: 1; |
| | | border-radius: 4px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .feed-content-types { |
| | | margin-bottom: 16px; |
| | | |
| | | .components-base-control__label { |
| | | margin-bottom: 8px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .checkbox-list { |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | max-height: 200px; |
| | | overflow-y: auto; |
| | | padding: 8px; |
| | | background: white; |
| | | |
| | | .components-checkbox-control { |
| | | margin: 4px 0; |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .select-all-wrapper { |
| | | margin-top: 8px; |
| | | padding-top: 8px; |
| | | border-top: 1px solid #ddd; |
| | | } |
| | | |
| | | .components-checkbox-control__input-container { |
| | | margin-right: 8px; |
| | | } |
| | | } |
| | | .feed-block { |
| | | border: 1px solid #ddd; |
| | | padding: 20px; |
| | | background: white; |
| | | |
| | | .feed-block-preview { |
| | | .filter-preview { |
| | | display: flex; |
| | | gap: 8px; |
| | | margin: 16px 0; |
| | | flex-wrap: wrap; |
| | | |
| | | .content-type-badge { |
| | | background: #f0f0f0; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .feed-grid-placeholder { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| | | gap: 16px; |
| | | margin-top: 20px; |
| | | |
| | | .grid-item-placeholder { |
| | | background: #f0f0f0; |
| | | aspect-ratio: 1; |
| | | border-radius: 4px; |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import save from './save'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | |
| | | /** |
| | | * @see ./save.js |
| | | */ |
| | | save, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | // Silence is golden. |
| New file |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| New file |
| | |
| | | export default function save() { |
| | | return null; // Dynamic block rendered by PHP |
| | | } |
| New file |
| | |
| | | //.feed-block { |
| | | // max-width: var(--full); |
| | | // margin: 0 auto; |
| | | // |
| | | // &:target { |
| | | // scroll-snap-margin-top: 5rem; |
| | | // scroll-margin-top: 5rem; |
| | | // outline: 0; |
| | | // border-radius: 0; |
| | | // padding: 0; |
| | | // |
| | | // .feed-item { |
| | | // outline: double var(--pink-0); |
| | | // } |
| | | // } |
| | | //} |
| | | // |
| | | //.loading .feed-block { |
| | | // opacity: .7; |
| | | //} |
| | | // |
| | | //.label { |
| | | // display: flex; |
| | | // align-items: center; |
| | | // gap: .25rem; |
| | | // font-size: .9rem; |
| | | // a:hover { |
| | | // color: var(--pink-0); |
| | | // } |
| | | //} |
| | | // |
| | | ///** Filters Form **/ |
| | | //.feed-filters { |
| | | // margin: 2rem 0; |
| | | // position: sticky; |
| | | // top: 3rem; |
| | | // z-index: 15; |
| | | // background: rgba(var(--base-rgb),var(--op-6)); |
| | | // padding: .25rem 3rem; |
| | | // details[open] summary { |
| | | // background-color: var(--overlay); |
| | | // } |
| | | // summary { |
| | | // justify-content: flex-start; |
| | | // |
| | | // > * { |
| | | // order: 3; |
| | | // } |
| | | // .label { |
| | | // order: 1; |
| | | // } |
| | | // .filter-label { |
| | | // order: 2; |
| | | // } |
| | | // &::after { |
| | | // order: 4; |
| | | // } |
| | | // #favourites + label { |
| | | // margin-left: auto; |
| | | // } |
| | | // #favourites + label:hover, |
| | | // #favourites:checked + label { |
| | | // border-color: var(--pink-0); |
| | | // background-color: var(--pink-0); |
| | | // color: var(--white); |
| | | // |
| | | // } |
| | | // #favourites:checked + label { |
| | | // animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
| | | // } |
| | | // } |
| | | // |
| | | // details[open], |
| | | // summary:hover { |
| | | // background-color: rgba(var(--base-rgb),var(--op-6)); |
| | | // } |
| | | // |
| | | // &:has(#favourites) { |
| | | // summary::after { |
| | | // margin-left: 1rem; |
| | | // } |
| | | // } |
| | | //} |
| | | //summary > * { |
| | | // order: 3; |
| | | //} |
| | | //summary .label { |
| | | // order: 1; |
| | | //} |
| | | //.filter-label { |
| | | // display: inline-block; |
| | | // vertical-align: middle; |
| | | // height: 1.3em; |
| | | // order: 2; |
| | | // margin: 0; |
| | | // padding: 0; |
| | | // |
| | | // li { |
| | | // list-style: none; |
| | | // height: 0; |
| | | // overflow: hidden; |
| | | // &.active { |
| | | // height: 100%; |
| | | // } |
| | | // } |
| | | //} |
| | | //.filter-group { |
| | | // display: flex; |
| | | // align-items: center; |
| | | // gap: .5rem; |
| | | // flex-wrap: wrap; |
| | | // label { |
| | | // font-weight: 100; |
| | | // } |
| | | // &:has(.order-by) { |
| | | // justify-content: space-between; |
| | | // } |
| | | // .order-by, |
| | | // .order-direction { |
| | | // display: flex; |
| | | // flex-wrap: wrap; |
| | | // gap: .5rem; |
| | | // flex: 1; |
| | | // justify-content: flex-start; |
| | | // .label { |
| | | // width: 100%; |
| | | // } |
| | | // |
| | | // label:has(.label) { |
| | | // padding: 0 .35rem; |
| | | // gap: .5rem; |
| | | // .label { |
| | | // font-size: 1rem; |
| | | // } |
| | | // } |
| | | // } |
| | | //} |
| | | //.filter-group >.label { |
| | | // width: 100%; |
| | | //} |
| | | // |
| | | ///** |
| | | //Feed Grid |
| | | // */ |
| | | //.feed-grid { |
| | | // display: grid; |
| | | // grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| | | // gap: .5rem; |
| | | // margin-bottom: 2rem; |
| | | // padding: 0 4rem; |
| | | // --delay: 0s; |
| | | // --increase: .1s; |
| | | //} |
| | | //.feed-empty-state { |
| | | // grid-column: 1/-1; |
| | | // text-align: center; |
| | | // padding: 2rem; |
| | | // background: var(--base-100); |
| | | // border-radius: var(--radius); |
| | | // margin: 0 auto; |
| | | // max-width: 600px; |
| | | //} |
| | | ///** |
| | | //Placeholders |
| | | // */ |
| | | //.placeholder { |
| | | // aspect-ratio: 1; |
| | | // background: var(--base); |
| | | // border: 1rem solid var(--base-50); |
| | | // border-radius: 1rem; |
| | | // display: flex; |
| | | // justify-content: center; |
| | | // align-items: center; |
| | | // |
| | | // .icon { |
| | | // --w: 50%; |
| | | // color: var(--base-200); |
| | | // animation: dance 2.5s ease-in-out infinite; |
| | | // |
| | | // } |
| | | //} |
| | | // |
| | | ///** |
| | | //Feed Items |
| | | // */ |
| | | //.feed-item { |
| | | // position: relative; |
| | | // border-radius: 0.5rem; |
| | | // overflow: hidden; |
| | | // background: var(--base-50); |
| | | // box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| | | // opacity: 0; |
| | | // transition: opacity var(--trans-base) var(--delay); |
| | | // height: fit-content; |
| | | // padding: 0; |
| | | // |
| | | // img { |
| | | // opacity: .7; |
| | | // filter: grayscale(.5) sepia(.3) blur(7px); |
| | | // } |
| | | // &[data-loaded=true] { |
| | | // img { |
| | | // opacity: 1; |
| | | // filter: none; |
| | | // } |
| | | // } |
| | | // |
| | | // a { |
| | | // &::before, |
| | | // &::after { |
| | | // display: none; |
| | | // } |
| | | // } |
| | | // details a { |
| | | // font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem); |
| | | // } |
| | | // |
| | | // &[data-loaded] { |
| | | // opacity: 1; |
| | | // + [data-loaded] { |
| | | // --delay: var(--delay) + var(--increase); |
| | | // } |
| | | // } |
| | | // |
| | | // &.highlighted { |
| | | // box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); |
| | | // animation: highlight-puls 2s ease-in-out; |
| | | // } |
| | | // &[open], |
| | | // &:hover { |
| | | // .handle { |
| | | // background-color: var(--overlay-pink-medium); |
| | | // backdrop-filter: blur(5px); |
| | | // } |
| | | // } |
| | | // summary { |
| | | // width: calc(100% - 1rem); |
| | | // height: 100%; |
| | | // aspect-ratio: 1; |
| | | // .handle { |
| | | // position: absolute; |
| | | // bottom: 0; |
| | | // left: 0; |
| | | // right: 0; |
| | | // background-color: rgba(var(--base-rgb),var(--op-3)); |
| | | // backdrop-filter: blur(5px); |
| | | // border-radius: var(--radius); |
| | | // z-index: 1; |
| | | // padding: .25rem .25rem .25rem 1.1rem; |
| | | // } |
| | | // &::after { |
| | | // z-index: 11; |
| | | // position: absolute; |
| | | // bottom: .35rem; |
| | | // right: .7rem; |
| | | // width: 1.5rem; |
| | | // height: 1.5rem; |
| | | // cursor: pointer; |
| | | // } |
| | | // } |
| | | // |
| | | // label { |
| | | // font-weight: normal; |
| | | // text-transform: none; |
| | | // .icon { |
| | | // --w: 1.5em; |
| | | // } |
| | | // } |
| | | //} |
| | | ///** |
| | | //Load More Button |
| | | // */ |
| | | //.load-more { |
| | | // opacity: 1; |
| | | // margin: 1rem auto; |
| | | // width: 66.666%; |
| | | // display: flex; |
| | | // align-items: center; |
| | | // gap: .5rem; |
| | | // padding: .75rem 1.5rem; |
| | | // background: var(--base); |
| | | // color: var(--contrast); |
| | | // border-radius: 4px; |
| | | // font-size: var(--txt-medium); |
| | | // transition: all var(--trans-base); |
| | | // border: 2px solid transparent; |
| | | // &[hidden] { |
| | | // opacity: 0; |
| | | // transition: all var(--trans-base); |
| | | // } |
| | | // &:hover { |
| | | // background: var(--pink-50); |
| | | // border-color: var(--contrast); |
| | | // color: var(--white); |
| | | // } |
| | | //} |
| | | ///** |
| | | //favourite button |
| | | // */ |
| | | //button.favourite { |
| | | // position: absolute; |
| | | // top: .5rem; |
| | | // right: .5rem; |
| | | // z-index: 10; |
| | | // background: rgba(var(--base-rgb),var(--op-4)); |
| | | // border-radius: 50%; |
| | | // box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw-subtle); |
| | | // border: none; |
| | | // width: 2rem; |
| | | // height: 2rem; |
| | | // display: flex; |
| | | // justify-content: center; |
| | | // align-items: center; |
| | | // backdrop-filter: blur(5px); |
| | | // transition: all var(--trans-base); |
| | | // |
| | | // &:hover { |
| | | // transform: scale(1.1); |
| | | // color: var(--pink-0); |
| | | // background: var(--base); |
| | | // box-shadow: 0 4px 8px rgba(0,0,0,0.15); |
| | | // } |
| | | // |
| | | // &.favourited { |
| | | // animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
| | | // } |
| | | //} |
| | | // |
| | | // |
| | | ///** Images **/ |
| | | //.feed-image { |
| | | // display: block; |
| | | // aspect-ratio: 1; |
| | | // overflow: hidden; |
| | | // width: 100%; |
| | | // height: 100%; |
| | | //} |
| | | //.feed-images { |
| | | // width: 100%; |
| | | // height: 100%; |
| | | // &.multi { |
| | | // display :grid; |
| | | // grid-template-columns: repeat(3, 1fr); |
| | | // grid-auto-rows: 1fr; |
| | | // gap: 4px; |
| | | // |
| | | // > a { |
| | | // width: 100%; |
| | | // height: 100%; |
| | | // aspect-ratio: 1; |
| | | // } |
| | | // .feed-image { |
| | | // grid-row: span 2; |
| | | // grid-column: span 2; |
| | | // } |
| | | // } |
| | | // img { |
| | | // width: 100%; |
| | | // height: 100%; |
| | | // object-fit: cover; |
| | | // transition: transform var(--trans-t) var(--trans-fn); |
| | | // } |
| | | // a:hover img { |
| | | // transform: scale(1.05); |
| | | // } |
| | | //} |
| | | // |
| | | //.feed-item { |
| | | // &:nth-of-type(4n+2) .multi .feed-image { |
| | | // grid-column: 2 / span 2; |
| | | // grid-row: 1 / span 2; |
| | | // } |
| | | // &:nth-of-type(4n+3) .multi .feed-image { |
| | | // grid-row: 2 / span 2; |
| | | // grid-column: 1 / span 2; |
| | | // } |
| | | // &:nth-of-type(4n+4) .multi .feed-image { |
| | | // grid-column: 2 / span 2; |
| | | // grid-row: 2 / span 2; |
| | | // } |
| | | //} |
| | | // |
| | | ///** Item Information **/ |
| | | //.item-info { |
| | | // padding: .25rem; |
| | | // border-left: 1px solid var(--base-200); |
| | | // >div + div { |
| | | // margin-top: .5em; |
| | | // position: relative; |
| | | // |
| | | // &::before { |
| | | // content: ''; |
| | | // display: block; |
| | | // position: absolute; |
| | | // top: -.3em; |
| | | // left: -.25rem; |
| | | // width: 66.6%; |
| | | // border-bottom: 1px solid var(--base-200); |
| | | // } |
| | | // } |
| | | // h3 { |
| | | // margin: 0 0 .5em 0!important; |
| | | // font-size: 1.1rem; |
| | | // font-family: var(--body); |
| | | // font-weight: var(--fw-b); |
| | | // } |
| | | // span { |
| | | // text-transform: uppercase; |
| | | // display: flex; |
| | | // align-items: center; |
| | | // } |
| | | // .icon { |
| | | // --w: 1.1em; |
| | | // margin-right: .5em; |
| | | // display: inline-block; |
| | | // vertical-align: middle; |
| | | // } |
| | | //} |
| | | //.item-list { |
| | | // ul { |
| | | // margin: 0; |
| | | // padding: .5em 0; |
| | | // display: flex; |
| | | // flex-wrap: wrap; |
| | | // gap: .5rem; |
| | | // li { |
| | | // list-style: none; |
| | | // } |
| | | // } |
| | | // a { |
| | | // background-color: var(--pink-0); |
| | | // border: 1px solid transparent; |
| | | // border-radius: 4px; |
| | | // color: var(--light-0); |
| | | // padding: .25em; |
| | | // line-height: .8; |
| | | // &:visited { |
| | | // background-color: var(--pink-100); |
| | | // color: var(--white); |
| | | // } |
| | | // &:visited:hover, |
| | | // &:visited:focus, |
| | | // &:hover, |
| | | // &:focus { |
| | | // background-color: transparent; |
| | | // border-color: var(--contrast); |
| | | // color: var(--contrast); |
| | | // } |
| | | // } |
| | | //} |
| | | ///** |
| | | //Loading |
| | | // */ |
| | | //.loading { |
| | | // opacity: .7; |
| | | //} |
| | | // |
| | | //.loading-overlay { |
| | | // position: fixed; |
| | | // top: 0; |
| | | // left: 0; |
| | | // right: 0; |
| | | // bottom: 0; |
| | | // background-color: rgba(var(--base-rgb),var(--op-4)); |
| | | // display: flex; |
| | | // align-items: center; |
| | | // justify-content: center; |
| | | // transition: opacity 0.3s ease, visibility 0.3s ease; |
| | | // z-index: 9999; |
| | | // opacity: 0; |
| | | // visibility: hidden; |
| | | //} |
| | | //.loading .loading-overlay { |
| | | // opacity: 1; |
| | | // visibility: visible; |
| | | // |
| | | // &::after { |
| | | // content: ''; |
| | | // position: absolute; |
| | | // z-index: -1; |
| | | // inset: 0; |
| | | // background: linear-gradient( |
| | | // 90deg, |
| | | // var(--shimmer) |
| | | // ); |
| | | // animation: shimmer 3s ease-in-out infinite; |
| | | // } |
| | | // |
| | | // .wrapper { |
| | | // background-color: rgba(var(--base-rgb),var(--op-6)); |
| | | // padding: 2rem; |
| | | // border-radius: var(--radius); |
| | | // text-align: center; |
| | | // max-width: 90%; |
| | | // width: 400px; |
| | | // height: 300px; |
| | | // z-index: 5; |
| | | // display: flex; |
| | | // justify-content: center; |
| | | // align-items: center; |
| | | // position: relative; |
| | | // |
| | | // .spinner { |
| | | // --h: 150px; |
| | | // --w: calc(var(--h) * 2); |
| | | // border-top: 5px solid var(--pink-0); |
| | | // border-radius: 50%; |
| | | // position: absolute; |
| | | // width: var(--w); |
| | | // height: var(--w); |
| | | // top: calc(50% - var(--h)); |
| | | // left: calc(50% - var(--h)); |
| | | // opacity: .5; |
| | | // z-index: 0; |
| | | // animation: spin 1s var(--trans-t) infinite; |
| | | // } |
| | | // div.icon { |
| | | // height: 50px; |
| | | // width: 50px; |
| | | // |
| | | // .icon { |
| | | // --w: 100%; |
| | | // svg { |
| | | // animation: dance 2s ease-in-out infinite; |
| | | // transition: color 0.3s ease; |
| | | // } |
| | | // } |
| | | // } |
| | | // .status { |
| | | // height: 200px; |
| | | // width: 100%; |
| | | // z-index: 5; |
| | | // display: flex; |
| | | // flex-direction: column; |
| | | // align-items: center; |
| | | // |
| | | // h3 { |
| | | // margin: 1.5rem 0 .25rem!important; |
| | | // color: var(--contrast-200); |
| | | // } |
| | | // .message { |
| | | // margin: 0; |
| | | // max-width: 275px; |
| | | // color: var(--contrast-100); |
| | | // font-size: var(--txt-x-small); |
| | | // animation: flicker 2s infinite; |
| | | // } |
| | | // } |
| | | // } |
| | | //} |
| | | // |
| | | // |
| | | ///* Animations */ |
| | | //@keyframes highlight { |
| | | // 0%, 100% { |
| | | // box-shadow: none; |
| | | // } |
| | | // 50% { |
| | | // box-shadow: 0 0 0 4px var(--pink-0); |
| | | // } |
| | | //} |
| | | // |
| | | //@keyframes pop { |
| | | // 0% { transform: scale(1); } |
| | | // 50% { transform: scale(1.3); } |
| | | // 75% { transform: scale(0.9); } |
| | | // 100% { transform: scale(1); } |
| | | //} |
| | | // |
| | | //@keyframes bubble { |
| | | // 50% { box-shadow: 19px 0 0 3px, 38px 0 0 7px, 57px 0 0 3px } |
| | | // 100% { box-shadow: 19px 0 0 0, 38px 0 0 3px, 57px 0 0 7px } |
| | | //} |
| | | //@keyframes highlight-pulse { |
| | | // 0%, 100% { box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); } |
| | | // 50% { box-shadow: 0 0 0 8px #FF0080, 0 12px 24px rgba(0, 0, 0, 0.15); } |
| | | //} |
| | | // |
| | | //@keyframes spin { |
| | | // to { transform: rotate(360deg); } |
| | | //} |
| | | // |
| | | //@keyframes shimmer { |
| | | // 0% { transform: translateX(-100%); } |
| | | // 50%, 100% { transform: translateX(100%); } |
| | | //} |
| | | //@keyframes dance { |
| | | // 0%, 100% { transform: rotate(-5deg) scale(1);} |
| | | // 50% { transform: rotate(5deg) scale(1.1); } |
| | | //} |
| | | //@keyframes flicker { |
| | | // 0% { opacity: 0.6; } |
| | | // 50% { opacity: 1; } |
| | | // 100% { opacity: 0.6; } |
| | | //} |
| | | // |
| | | // |
| | | ///** |
| | | //Accessibility |
| | | // */ |
| | | // |
| | | ///* Keyboard navigable feed items */ |
| | | //.feed-item[tabindex="0"] { |
| | | // position: relative; |
| | | //} |
| | | // |
| | | //.feed-item[tabindex="0"]::after { |
| | | // content: ''; |
| | | // position: absolute; |
| | | // top: 0; |
| | | // left: 0; |
| | | // right: 0; |
| | | // bottom: 0; |
| | | // pointer-events: none; |
| | | // border: 2px solid transparent; |
| | | // transition: border-color 0.2s ease; |
| | | //} |
| | | // |
| | | //.feed-item[tabindex="0"]:focus::after { |
| | | // border-color: #FF0080; |
| | | //} |
| | | // |
| | | // |
| | | //.feed-block .item { |
| | | // summary { |
| | | // a { |
| | | // background-color: var(--action-0); |
| | | // display: flex; |
| | | // gap: .25rem; |
| | | // flex-wrap: nowrap; |
| | | // aspect-ratio: 1; |
| | | // position: relative; |
| | | // } |
| | | // img { |
| | | // width: 50%; |
| | | // height: 100%; |
| | | // object-fit: cover; |
| | | // } |
| | | // |
| | | // } |
| | | //} |
| | | |
| | | |
| | | .feed-block { |
| | | grid-column: full; |
| | | .filters { |
| | | padding: 1rem 0; |
| | | max-width:var(--wide); |
| | | margin: 0 auto; |
| | | |
| | | .remove-term.remove-term { |
| | | width: max-content; |
| | | height: max-content |
| | | } |
| | | } |
| | | .filter-group { |
| | | position: relative; |
| | | padding: 2rem 0; |
| | | .label { |
| | | position: absolute; |
| | | left: 0; |
| | | } |
| | | > .label { |
| | | top: 0; |
| | | } |
| | | [type=radio] { |
| | | position:absolute; |
| | | left: var(--offScreen); |
| | | } |
| | | button, label { |
| | | position: relative; |
| | | padding: .5rem; |
| | | height: max-content; |
| | | &:hover { |
| | | color: var(--action-contrast); |
| | | } |
| | | } |
| | | button:hover .label, |
| | | :checked + label .label, |
| | | label:hover .label { |
| | | opacity: 1; |
| | | visibility: visible; |
| | | } |
| | | &:has(label:hover) :checked + label .label, |
| | | button .label, |
| | | label .label { |
| | | --height: max-content; |
| | | opacity: 0; |
| | | visibility: hidden; |
| | | bottom: -2rem; |
| | | width: max-content; |
| | | white-space: nowrap; |
| | | font-weight: var(--fw-b); |
| | | } |
| | | |
| | | |
| | | } |
| | | h3 { |
| | | margin: 0 0 .25rem; |
| | | font-size: var(--medium); |
| | | } |
| | | } |
| | | /** PLACEHOLDERS **/ |
| | | .placeholder { |
| | | aspect-ratio: 1; |
| | | background: var(--base); |
| | | border: 1rem solid var(--base-50); |
| | | border-radius: 1rem; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | |
| | | i.icon { |
| | | --w: 50%; |
| | | color: var(--base-200); |
| | | animation: dance 2.5s ease-in-out infinite; |
| | | } |
| | | } |
| | | |
| | | .item-grid { |
| | | padding: 0 var(--chip); |
| | | max-width: 100%; |
| | | } |
| | | /** FEED ITEM **/ |
| | | .feed.item { |
| | | position: relative; |
| | | border-radius: 0.5rem; |
| | | overflow: hidden; |
| | | background: var(--base-50); |
| | | box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| | | height: fit-content; |
| | | padding: 0; |
| | | details { |
| | | z-index: var(--z-2); |
| | | width: 100%; |
| | | position: relative; |
| | | padding: 0; |
| | | summary { |
| | | position:absolute; |
| | | top: -3rem; |
| | | left:0; |
| | | width: 100%; |
| | | background-color: rgba(var(--base-rgb),var(--op-2)); |
| | | backdrop-filter: blur(5px); |
| | | &:hover { |
| | | background-color: rgba(var(--action-rgb),var(--op-45)); |
| | | } |
| | | } |
| | | &[open] { |
| | | padding: .25rem .5rem; |
| | | summary .icon { |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | img { |
| | | object-fit: cover; |
| | | width: 100%; |
| | | height: 100%; |
| | | //opacity: .7; |
| | | //filter: grayscale(.5) sepia(.3); |
| | | &:hover { |
| | | opacity: .8; |
| | | } |
| | | } |
| | | |
| | | &[data-timeline] { |
| | | .images { |
| | | aspect-ratio: 3/2; |
| | | padding: 0 0 1rem; |
| | | span { |
| | | width: 50%; |
| | | position: absolute; |
| | | background-color: var(--action-0); |
| | | color: var(--action-contrast); |
| | | padding: .25rem .5rem; |
| | | &:first-of-type { |
| | | bottom: 0; |
| | | right: 50%; |
| | | text-align: right; |
| | | } |
| | | &:last-of-type { |
| | | top: 0; |
| | | left: 50%; |
| | | } |
| | | } |
| | | > a { |
| | | position: relative; |
| | | display: flex; |
| | | flex-wrap: nowrap; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | } |
| | | img { |
| | | width: 50%; |
| | | object-fit: cover; |
| | | height: 100%; |
| | | &:first-of-type { |
| | | border-right: 1px solid var(--action-0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | a { |
| | | &::before, |
| | | &::after { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | label { |
| | | font-weight: normal; |
| | | text-transform: none; |
| | | .icon { |
| | | --w: 1.5em; |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | .item-grid:has([data-timeline]) { |
| | | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
| | | } |
| | | |
| | | .items-wrap [type=radio], |
| | | .items-wrap [type=checkbox] { |
| | | position: absolute; |
| | | opacity: 0; |
| | | left: -200vw; |
| | | } |
| | | |
| | | .items-wrap [type=radio] + label, |
| | | .items-wrap [type=checkbox] + label { |
| | | position: relative; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .items-wrap [type=radio] + label:hover, |
| | | .items-wrap [type=checkbox] + label:hover { |
| | | color: var(--action-0); |
| | | } |
| | | |
| | | .items-wrap [type=radio] + label::before, |
| | | .items-wrap [type=checkbox] + label::before, |
| | | .items-wrap [type=radio] + label::after, |
| | | .items-wrap [type=checkbox] + label::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 50%; |
| | | } |
| | | |
| | | .items-wrap [type=radio] + label::after, |
| | | .items-wrap [type=checkbox] + label::after { |
| | | left: 5px; |
| | | transform: translateY(-70%) rotate(45deg); |
| | | width: 5px; |
| | | height: 10px; |
| | | border: solid var(--light-0); |
| | | border-width: 0 2px 2px 0; |
| | | display: none; |
| | | } |
| | | |
| | | .items-wrap [type=radio] + label::before, |
| | | .items-wrap [type=checkbox] + label::before { |
| | | left: 0; |
| | | transform: translateY(-50%); |
| | | width: 1rem; |
| | | height: 1rem; |
| | | border: 2px solid var(--contrast-200); |
| | | background-color: var(--base); |
| | | border-radius: var(--radius); |
| | | } |
| | | |
| | | .items-wrap [type=radio]:hover + label::before, |
| | | .items-wrap [type=checkbox]:hover + label::before { |
| | | border-color: var(--action-200); |
| | | } |
| | | |
| | | .items-wrap [type=radio]:checked + label::before, |
| | | .items-wrap [type=checkbox]:checked + label::before{ |
| | | background-color: var(--action-0); |
| | | border-color: var(--action-100); |
| | | } |
| | | |
| | | .items-wrap [type=radio]:checked + label::before { |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | .items-wrap [type=checkbox]:checked + label::after { |
| | | display: block; |
| | | left: 5px; |
| | | top: 50%; |
| | | transform: translateY(-70%) rotate(45deg); |
| | | width: .35rem; |
| | | height: .66rem; |
| | | border: solid var(--light-0); |
| | | border-width: 0 2px 2px 0; |
| | | } |
| | | |
| | | .items-wrap :disabled + label { |
| | | cursor: not-allowed; |
| | | background-color: var(--base-50); |
| | | color: var(--base-200); |
| | | border-color: var(--base-200); |
| | | } |
| | | |
| | | .items-wrap :disabled + label:hover { |
| | | background-color: var(--base-50); |
| | | color: var(--base-200); |
| | | border-color: var(--base-200); |
| | | } |
| | | |
| | | .items-wrap :disabled + label::before { |
| | | border-color: var(--base-200); |
| | | } |
| | | |
| | | #jvb-selector .items-wrap [type=radio] + label, |
| | | #jvb-selector .items-wrap [type=checkbox] + label{ |
| | | flex: 1; |
| | | padding-left: 2rem!important; |
| | | transform-origin: top center; |
| | | will-change: transform; |
| | | } |
| | | |
| | | .feed-block + footer { |
| | | grid-column: full; |
| | | padding: 0!important; |
| | | margin: 0; |
| | | background-color: var(--base-50); |
| | | z-index: 0; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | button { |
| | | width: max-content; |
| | | margin-left: auto; |
| | | padding: .35rem .25rem; |
| | | --w: 1.3em!important; |
| | | flex-wrap: nowrap; |
| | | justify-content:flex-start; |
| | | transition: var(--trans-size); |
| | | font-size: var(--txt-x-small); |
| | | min-height: 0; |
| | | span { |
| | | display: none; |
| | | white-space: nowrap; |
| | | } |
| | | &:focus span, |
| | | &:hover span { |
| | | display: block; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | class FeedBlock { |
| | | constructor() { |
| | | this.container = document.querySelector('section.feed-block'); |
| | | if(!this.container) return; |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | this.error = window.jvbError; |
| | | this.cache = new window.jvbCache('feed'); |
| | | this.templates = window.jvbTemplates; |
| | | |
| | | this.config = { |
| | | source: '', |
| | | context: '', |
| | | highlight: null, |
| | | gallery: false, |
| | | view: this.cache.get('feedView') || 'grid', |
| | | ... this.container.dataset |
| | | }; |
| | | |
| | | this.init(); |
| | | } |
| | | init() { |
| | | this.initElements(); |
| | | this.defineTemplates(); |
| | | this.initListeners(); |
| | | this.initFilters(); |
| | | |
| | | if ('requestIdleCallback' in window) { |
| | | requestIdleCallback(() => { |
| | | this.initStore(); |
| | | this.initTaxonomies(); |
| | | |
| | | this.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, { timeout: 2000 }); |
| | | } else { |
| | | setTimeout(() => { |
| | | this.initStore(); |
| | | this.initTaxonomies(); |
| | | |
| | | this.processCachedFilters(); |
| | | this.processURLFilters(); |
| | | this.updateFilterUI(); |
| | | this.initGallery(); |
| | | }, 100); |
| | | } |
| | | } |
| | | |
| | | initElements() { |
| | | this.selectors = { |
| | | filterTrigger: '[data-filter]', |
| | | filters: { |
| | | actions: '.filter-actions .toggle-text', |
| | | container: '.filters', |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | | match: '[data-filter="match"]', |
| | | favourites: '[data-filter="favourites"]', |
| | | taxonomy: '[data-filter^="taxonomy"]', |
| | | }, |
| | | grid: '.item-grid', |
| | | selected: '.selected-items', |
| | | buttons: { |
| | | loadMore: 'button.load-more', |
| | | remove: '.remove-term', |
| | | clearFilters: 'button.clear-filters', |
| | | refresh: 'button[data-action="refresh"]' |
| | | } |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.selectors, this.container); |
| | | this.ui.buttons.refresh = document.querySelector(this.selectors.buttons.refresh); |
| | | |
| | | //Add content and taxonomies |
| | | this.ui.content = this.ui.filters.container.querySelectorAll('[name="content"]'); |
| | | if (this.ui.content.length === 0) this.ui.content = false; |
| | | this.ui.taxonomies = this.ui.filters.container.querySelectorAll('[data-taxonomy]'); |
| | | if (this.ui.taxonomies.length === 0) this.ui.taxonomies = false; |
| | | this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]'); |
| | | if (this.ui.orderbyWrap.length === 0) this.ui.orderbyWrap = false; |
| | | this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]'); |
| | | if (this.ui.order.length === 0) this.ui.order = false; |
| | | this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'); |
| | | if (this.ui.orderby.length === 0) this.ui.orderby = false; |
| | | |
| | | this.orderbyFilters = (this.ui.orderby) |
| | | ? Array.from(this.ui.orderby).map(o => o.value) |
| | | : []; |
| | | |
| | | this.contentTypes = (this.ui.content) |
| | | ? Array.from(this.ui.content).map(c => c.value) |
| | | : [this.container.dataset.content]; |
| | | this.taxonomies = (this.ui.taxonomies?.length > 0) |
| | | ? Array.from(this.ui.taxonomies).map(t => t.dataset.taxonomy) |
| | | : []; |
| | | } |
| | | |
| | | initListeners() { |
| | | this.popStateHandler = this.handlePopState.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | |
| | | window.addEventListener('popstate', this.popStateHandler); |
| | | document.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('change', this.changeHandler); |
| | | } |
| | | |
| | | initFilters() { |
| | | this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match']; |
| | | let defaults = { |
| | | content: this.contentTypes[0], |
| | | orderby: 'date', |
| | | order: 'desc', |
| | | page: 1, |
| | | }; |
| | | if (this.config.context) defaults.context = this.config.context; |
| | | if (this.config.source) defaults.source = this.config.source; |
| | | |
| | | this.filters = defaults; |
| | | this.defaults = {...defaults}; |
| | | } |
| | | updateFilterUI() { |
| | | if (this.ui.filters.container) { |
| | | //Get cached inputs |
| | | let groups = [ |
| | | this.ui.content, |
| | | this.ui.orderby, |
| | | this.ui.order |
| | | ]; |
| | | |
| | | groups.forEach(group => { |
| | | if(group) { |
| | | for (let input of group) { |
| | | let [filter, value] = [input.dataset.filter, input.value]; |
| | | if (!Object.hasOwn(this.store.filters, filter)) break; |
| | | let doit = this.store.filters[filter] === value; |
| | | if (doit) { |
| | | input.checked = doit; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | |
| | | if (Object.hasOwn(this.store.filters, 'taxonomy')) { |
| | | for (let [taxonomy, terms] of Object.entries(this.store.filters.taxonomy)) { |
| | | terms.forEach(termId => { |
| | | termId = parseInt(termId); |
| | | const term = this.selector.store.get(termId); |
| | | if (term) { |
| | | this.createTermElement(termId); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | handlePopState(e) { |
| | | if (e.state?.filters) { |
| | | if (this.processURLFilters()) { |
| | | this.store.setFilters(this.filters); |
| | | this.a11y.announce('Feed filters updated from browser history'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | handleClick(e) { |
| | | if (window.targetCheck(e, this.selectors.buttons.loadMore)) { |
| | | this.nextPage(); |
| | | } else if (window.targetCheck(e, this.selectors.buttons.clearFilters)) { |
| | | this.clearFilters(); |
| | | } |
| | | let remove = window.targetCheck(e, this.selectors.buttons.remove); |
| | | if (remove) { |
| | | this.removeSelectedTerm(remove); |
| | | } |
| | | |
| | | let refresh = window.targetCheck(e, this.selectors.buttons.refresh); |
| | | if (refresh) { |
| | | this.store.clearCache(); |
| | | this.store.fetch(); |
| | | } |
| | | |
| | | let orderbyButton = window.targetCheck(e, '[data-filter="orderby"]'); |
| | | if (orderbyButton && orderbyButton.value === 'random' && orderbyButton.checked) { |
| | | // Already selected random, just re-render to trigger new shuffle |
| | | this.renderItems(); |
| | | } |
| | | } |
| | | |
| | | nextPage() { |
| | | const nextPage = (this.store.filters.page || 1) + 1; |
| | | const maxPage = this.store.lastResponse?.pages || nextPage; |
| | | this.store.setFilters({ page: Math.min(nextPage, maxPage) }); |
| | | } |
| | | |
| | | handleChange(e) { |
| | | const target = e.target; |
| | | if (Object.hasOwn(target.dataset, 'filter')) { |
| | | if (this.allowedFilters.includes(target.dataset.filter)) { |
| | | let filters = {}; |
| | | filters[target.dataset.filter] = target.value; |
| | | this.resetFilters(filters); |
| | | } |
| | | switch (target.dataset.filter) { |
| | | case 'content': |
| | | this.updateContentFor(target.value); |
| | | break; |
| | | case 'orderby': |
| | | this.updateOrderOptions(target.value); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | clearFilters() { |
| | | this.taxFilters = {}; |
| | | window.removeChildren(this.ui.selected); |
| | | |
| | | this.taxonomies.forEach(tax => { |
| | | let fieldId = this.getFieldId(tax); |
| | | this.selector.selectedTerms.get(fieldId)?.clear(); |
| | | }); |
| | | |
| | | this.store.setFilters({ |
| | | ...this.defaults, |
| | | taxonomy: null |
| | | }); |
| | | |
| | | this.updateURL(); |
| | | this.saveToCacheFilters(); |
| | | } |
| | | |
| | | resetFilters(filters) { |
| | | filters = { |
| | | ...this.store.filters, |
| | | page: 1, |
| | | ... filters |
| | | } |
| | | this.store.setFilters(filters); |
| | | |
| | | this.updateURL(); |
| | | this.saveToCacheFilters(); |
| | | } |
| | | |
| | | getFieldId(taxonomy) { |
| | | return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null); |
| | | } |
| | | removeSelectedTerm(button) { |
| | | const termId = parseInt(button.dataset.id); |
| | | const taxonomy = button.dataset.taxonomy; |
| | | |
| | | if (Object.hasOwn(this.taxFilters, taxonomy)){ |
| | | this.taxFilters[taxonomy] = this.taxFilters[taxonomy] |
| | | .filter(id => id !== termId); |
| | | if (this.taxFilters[taxonomy].length === 0) { |
| | | delete this.taxFilters[taxonomy]; |
| | | } |
| | | } |
| | | button.remove(); |
| | | |
| | | // Find the fieldId for this taxonomy |
| | | const field = this.getFieldId(taxonomy); |
| | | if (field) { |
| | | this.selector.activeField = field; |
| | | // Notify selector to remove from its selectedTerms |
| | | this.selector.removeSelected(termId, field); |
| | | } |
| | | |
| | | this.resetFilters({ |
| | | taxonomy: Object.keys(this.taxFilters).length > 0 |
| | | ? this.taxFilters |
| | | : null |
| | | }); |
| | | } |
| | | |
| | | updateContentFor(content) { |
| | | let checkIt = [ |
| | | this.ui.taxonomies, |
| | | this.ui.orderby |
| | | ]; |
| | | checkIt.forEach(check => { |
| | | if (!check) return; |
| | | check.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',')??[]; |
| | | button.hidden = forTypes.length > 0 && !forTypes.includes(content); |
| | | if (button.hidden && button.checked) { |
| | | button.checked = false; |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | updateOrderOptions(order) { |
| | | if (this.ui.orderbyWrap) { |
| | | let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[]; |
| | | this.ui.orderbyWrap.hidden = !options.includes(order); |
| | | } |
| | | } |
| | | |
| | | updateFilterControls() { |
| | | const isHidden = Object.keys(this.taxFilters).length === 0; |
| | | if (this.ui.buttons.clearFilters) { |
| | | this.ui.buttons.clearFilters.hidden = isHidden; |
| | | } |
| | | if (this.ui.filters.actions) { |
| | | this.ui.filters.actions.hidden = isHidden; |
| | | } |
| | | } |
| | | |
| | | async initTaxonomies() { |
| | | this.taxFilters = {}; |
| | | this.selector = window.jvbSelector; |
| | | // this.selector.scanExistingFields(this.ui.filters.container); |
| | | // this.taxonomies.map(tax => this.selector.batchFetch.add(tax)); |
| | | // this.selector.batchFetchTaxonomies(); |
| | | this.selector.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'selected-terms': |
| | | |
| | | this.handleTaxonomyChange(data); |
| | | break; |
| | | |
| | | } |
| | | }); |
| | | } |
| | | handleTaxonomyChange(data) { |
| | | const {terms, taxonomy } = data; |
| | | if (terms.size === 0) return; |
| | | this.taxFilters[taxonomy] = Array.from(terms); |
| | | this.resetFilters({ taxonomy: this.taxFilters }); |
| | | |
| | | terms.forEach(t => { |
| | | this.createTermElement(t); |
| | | }); |
| | | this.updateFilterControls(); |
| | | } |
| | | getTaxonomyIcon(taxonomy) { |
| | | let iconButton = Array.from(this.ui.taxonomies) |
| | | .find(t => t.dataset.taxonomy === taxonomy); |
| | | return iconButton?.dataset.icon.trim() || 'tag'; |
| | | } |
| | | createTermElement(termId) { |
| | | const term = this.selector.store.get(termId); |
| | | if (!term) return; |
| | | if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return; |
| | | |
| | | term.icon = this.getTaxonomyIcon(term.taxonomy); |
| | | this.ui.selected.append(this.templates.create('feedTerm', term)); |
| | | } |
| | | |
| | | processCachedFilters() { |
| | | Object.keys(this.filters).forEach(filter => { |
| | | let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`); |
| | | if (cached && cached !== this.filters[filter]) { |
| | | this.filters[filter] = cached; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | processURLFilters() { |
| | | if (!this.isFirstPage()) return false; |
| | | const params = new URLSearchParams(window.location.search); |
| | | if (!params.toString()) return false; |
| | | let shouldUpdate = false; |
| | | this.allowedFilters.forEach(filter => { |
| | | let value = params.get(`f_${filter}`); |
| | | if (value) { |
| | | shouldUpdate = true; |
| | | this.filters[filter] = value; |
| | | } |
| | | }); |
| | | |
| | | let hasTax = false; |
| | | params.forEach((value, key) => { |
| | | if (key.startsWith('f_tax_')) { |
| | | hasTax = true; |
| | | shouldUpdate = true; |
| | | const taxonomy = key.replace('f_tax_',''); |
| | | this.taxFilters[taxonomy] = value.split(',').map(Number); |
| | | } |
| | | }); |
| | | if (shouldUpdate) { |
| | | if (hasTax) { |
| | | this.filters.taxonomy = this.taxFilters; |
| | | } |
| | | this.resetFilters(this.filters); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | updateURL() { |
| | | const params = new URLSearchParams(); |
| | | this.allowedFilters.forEach(key => { |
| | | if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) { |
| | | params.set(`f_${key}`, this.store.filters[key]); |
| | | } |
| | | }); |
| | | |
| | | for (let [tax, terms] of Object.entries(this.taxFilters)) { |
| | | if (terms.length > 0) { |
| | | params.set(`f_tax_${tax}`, terms.join(',')); |
| | | } |
| | | } |
| | | |
| | | const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`; |
| | | const currentURL = window.location.pathname + window.location.search; // Change this line |
| | | |
| | | if (newURL !== currentURL) { |
| | | window.history.pushState({filters:this.store.filters}, '', newURL); |
| | | } |
| | | } |
| | | |
| | | saveToCacheFilters() { |
| | | Object.keys(this.store.filters).forEach(filter => { |
| | | const cacheKey = `${this.config.source}_${this.config.context}_${filter}`; |
| | | |
| | | if (this.store.filters[filter] !== this.defaults[filter]) { |
| | | this.cache.set(cacheKey, this.store.filters[filter]); |
| | | } else { |
| | | this.cache.remove(cacheKey); |
| | | } |
| | | }); |
| | | |
| | | const taxCacheKey = `${this.config.source}_${this.config.context}_taxonomy`; |
| | | if (Object.keys(this.taxFilters).length > 0) { |
| | | this.cache.set(taxCacheKey, this.taxFilters); |
| | | } else { |
| | | this.cache.remove(taxCacheKey); |
| | | } |
| | | } |
| | | |
| | | initGallery() { |
| | | this.gallery = (this.config.gallery) ? window.jvbGallery : false; |
| | | if (this.gallery) { |
| | | this.gallery.subscribe((event, data) => { |
| | | if (event === 'load-more' && this.store.lastResponse?.has_more) { |
| | | this.nextPage(); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | initStore() { |
| | | let extraOrderby = this.orderbyFilters.filter(v => !['date','modified','title','random'].includes(v)); |
| | | let extraIndexes = []; |
| | | extraOrderby.forEach(orderby =>{ |
| | | extraIndexes.push({name:orderby, keyPath: orderby}); |
| | | }); |
| | | const store = window.jvbStore.register( |
| | | 'feed', |
| | | { |
| | | storeName: 'feed', |
| | | endpoint: 'feed', |
| | | keyPath: 'id', |
| | | indexes: [ |
| | | { name: 'content', keyPath: 'content'}, |
| | | { name: 'taxonomy', keyPath: 'taxonomy'}, |
| | | { name: 'user', keyPath: 'user'}, |
| | | { name: 'date', keyPath: 'date'}, |
| | | { name: 'modified', keyPath: 'modified'}, |
| | | { name: 'title', keyPath: 'title'}, |
| | | ... extraIndexes |
| | | ], |
| | | filters: this.filters, |
| | | TTL: 6 * 60 * 60 * 1000, //6 hours |
| | | showLoading: true, |
| | | required: 'content', |
| | | } |
| | | ); |
| | | |
| | | this.store = store.feed; |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | this.renderItems(data.items); |
| | | this.ui.buttons.loadMore.hidden = true; |
| | | if (this.store.lastResponse && this.store.lastResponse?.has_more) { |
| | | this.ui.buttons.loadMore.hidden = !this.store.lastResponse?.has_more??true; |
| | | } |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | isFirstPage() { |
| | | return this.store.filters.page === 1; |
| | | } |
| | | |
| | | renderItems(items = null) { |
| | | items = items??this.store.getFiltered(); |
| | | if (this.isFirstPage()) { |
| | | window.removeChildren(this.ui.grid); |
| | | } |
| | | if (items.length === 0) { |
| | | this.showEmptyState(); |
| | | this.a11y.announceItems(0, this.isFirstPage()); |
| | | } else { |
| | | window.chunkIt( |
| | | items, |
| | | (item) => this.createItemElement(item), |
| | | (fragment) => { |
| | | this.removePlaceholders(); |
| | | this.ui.grid.append(fragment); |
| | | if (this.config.gallery) this.gallery.buildGalleryItems('.item img'); |
| | | this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false); |
| | | }, |
| | | 5 |
| | | ).then(()=>{}); |
| | | } |
| | | |
| | | this.updateFilterControls(); |
| | | } |
| | | |
| | | showEmptyState() { |
| | | window.removeChildren(this.ui.grid); |
| | | this.ui.grid.append(this.templates.create('emptyState')); |
| | | } |
| | | |
| | | createItemElement(item) { |
| | | if (typeof item !== 'object') { |
| | | item = this.store.get(item); |
| | | if (!item) return; |
| | | } |
| | | return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item); |
| | | } |
| | | splitIDs(value) { |
| | | return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value); |
| | | } |
| | | |
| | | isImageField(item, value) { |
| | | if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) { |
| | | return false; |
| | | } |
| | | let values = this.splitIDs(value); |
| | | |
| | | return values.some(v => |
| | | Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v)) |
| | | ); |
| | | } |
| | | formatImageFields(element, value, item) { |
| | | let values = this.splitIDs(value); // Convert string to array first |
| | | if (values.length === 0) return; |
| | | |
| | | if (values.length > 1) { |
| | | let image = element.querySelector('img'); |
| | | if (!image) return; |
| | | values.forEach(imgID => { |
| | | let img = image.cloneNode(true); |
| | | this.formatImageField(img, imgID, item); |
| | | element.append(img); |
| | | }); |
| | | image.remove(); |
| | | } else { |
| | | if (element.tagName !== 'IMG') { |
| | | element = element.querySelector('img'); |
| | | if (!element) return; |
| | | } |
| | | this.formatImageField(element, values[0], item); |
| | | } |
| | | } |
| | | formatImageField(element, value, item) { |
| | | let imgData = item.images[value]??false; |
| | | if (!imgData) return; |
| | | [ |
| | | element.src, |
| | | element.srcset, |
| | | element.alt |
| | | ] = [ |
| | | imgData.tiny, |
| | | `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`, |
| | | imgData['image-alt-text'] |
| | | ] |
| | | } |
| | | isTaxonomyField(item, field) { |
| | | if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) { |
| | | return false; |
| | | } |
| | | |
| | | return Object.keys(item.taxonomies).includes(field); |
| | | } |
| | | formatTaxonomyField(element, item, field, value) { |
| | | if (element.tagName !== 'UL' || !element.querySelector('li')) return; |
| | | let values = this.splitIDs(value); |
| | | if (values.length === 0) { |
| | | element.remove(); |
| | | } |
| | | let listItem = element.querySelector('li'); |
| | | for (let termID of values) { |
| | | let term = item.taxonomies[field][termID]??false; |
| | | if (!term) continue; |
| | | let termItem = listItem.cloneNode(true); |
| | | let link = termItem.querySelector('a'); |
| | | if (!link) continue; |
| | | |
| | | let title = window.decodeHTMLEntities(term.title); |
| | | |
| | | [ |
| | | link.href, |
| | | link.title, |
| | | link.textContent |
| | | ] = [ |
| | | term.url, |
| | | `See more ${title}`, |
| | | title |
| | | ]; |
| | | element.append(termItem); |
| | | } |
| | | listItem.remove(); |
| | | } |
| | | isTimeField(el) { |
| | | return el.tagName === 'TIME' || el.querySelector('time') !== null; |
| | | } |
| | | formatTimeField(element, value) { |
| | | if (element.tagName !== 'TIME') { |
| | | element = element.querySelector('time'); |
| | | if (!element) return; |
| | | } |
| | | element.setAttribute('datetime', value); |
| | | element.textContent = window.formatTimeAgo(value, 'F Y'); |
| | | } |
| | | formatField(element, value) { |
| | | element.textContent = window.decodeHTMLEntities(value); |
| | | } |
| | | |
| | | addTimelineElements(item, template) { |
| | | let [ |
| | | afterEl, |
| | | number, |
| | | started, |
| | | last |
| | | ] = [ |
| | | template.querySelector('span.after-text'), |
| | | template.querySelector('[data-field="number"] b'), |
| | | template.querySelector('[data-field="started"] time'), |
| | | template.querySelector('[data-field="updated"] time') |
| | | ]; |
| | | |
| | | if (afterEl) { |
| | | afterEl.textContent = `After ${item.number - 1} Tx`; |
| | | } |
| | | if (number) { |
| | | number.textContent = item.number - 1; |
| | | } |
| | | if (started) { |
| | | this.formatTimeField(started, item.fields.timeline[0]['post_date']); |
| | | } |
| | | if (last) { |
| | | this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']); |
| | | } |
| | | } |
| | | |
| | | removePlaceholders() { |
| | | const placeholders = this.ui.grid.querySelectorAll('.placeholder'); |
| | | if (placeholders.length > 0) { |
| | | placeholders.forEach(p => p.remove()); |
| | | } |
| | | } |
| | | |
| | | defineTemplates() { |
| | | const T = this.templates; |
| | | const f = this; |
| | | |
| | | T.define('feedTerm', { |
| | | refs: { |
| | | icon: '.icon', |
| | | span: 'span' |
| | | }, |
| | | setup({el, refs, manyRefs, data}) { |
| | | el.dataset.id = data.id; |
| | | el.dataset.taxonomy = data.taxonomy; |
| | | if (refs.icon) refs.icon.className = `icon icon=${data.icon}`; |
| | | if (refs.span) refs.span.textContent = window.decodeHTMLEntities(data.name); |
| | | } |
| | | }); |
| | | T.define('emptyState'); |
| | | |
| | | this.contentTypes.forEach(content => { |
| | | T.define(`feedItem${window.uppercaseFirst(content)}`, { |
| | | refs: { |
| | | link: 'a', |
| | | }, |
| | | manyRefs: { |
| | | fields: '[data-field]', |
| | | }, |
| | | setup({el, refs, manyRefs, data}) { |
| | | const isTimeline = Object.hasOwn(el.dataset, 'timeline'); |
| | | if (manyRefs.fields) { |
| | | for (let field of manyRefs.fields) { |
| | | if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue; |
| | | |
| | | const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false; |
| | | if (!value) { |
| | | field.remove(); |
| | | continue; |
| | | } |
| | | if (f.isImageField(data, value)) { |
| | | f.formatImageField(field, value, data); |
| | | } else if (f.isTaxonomyField(data, field.dataset.field)) { |
| | | f.formatTaxonomyField(field, data, field.dataset.field, value); |
| | | } else if (f.isTimeField(field)) { |
| | | f.formatTimeField(field, value); |
| | | } else { |
| | | f.formatField(field, value); |
| | | } |
| | | } |
| | | if (refs.link && data.url !== '') { |
| | | refs.link.href = data.url; |
| | | refs.link.title = `View ${data.fields['post_title']??'Item'}`; |
| | | } |
| | | if (isTimeline ) f.addTimelineElements(data, el); |
| | | } |
| | | } |
| | | }) |
| | | }); |
| | | } |
| | | |
| | | // addPlaceholders() { |
| | | // let total = this.contentTypes.length; |
| | | // const fragment = document.createDocumentFragment(); |
| | | // for (let i = 0; i < 12; i++) { |
| | | // let template = window.getTemplate('placeholderTemplate'); |
| | | // |
| | | // let rand = Math.floor(Math.random() * total); |
| | | // let icon; |
| | | // if (this.ui.content && this.ui.content.length > 0) { |
| | | // icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true); |
| | | // } else { |
| | | // icon = window.getIcon(this.container.dataset.icon); |
| | | // } |
| | | // template.append(icon); |
| | | // fragment.append(template); |
| | | // } |
| | | // this.ui.grid.append(fragment); |
| | | // } |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | window.feedBlock = new FeedBlock(); |
| | | } |
| | | }); |
| | | }); |
| New file |
| | |
| | | class FeedBlockOld { |
| | | constructor() { |
| | | this.container = document.querySelector('section.feed-block'); |
| | | if (!this.container) { |
| | | return; |
| | | } |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | this.cache = new window.jvbCache('feed'); |
| | | this.error = window.jvbError; |
| | | |
| | | this.config = { |
| | | source: '', |
| | | context: '', |
| | | highlight: null, |
| | | gallery: false, |
| | | view: this.cache.get('feedView') || 'grid', |
| | | ... this.container.dataset |
| | | }; |
| | | this.initElements(); |
| | | this.initFilters(); |
| | | |
| | | |
| | | this.loadWhenAble(); |
| | | } |
| | | |
| | | loadWhenAble() { |
| | | if ('requestIdleCallback' in window) { |
| | | requestIdleCallback(() => { |
| | | this.initTaxonomies(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.initGallery(); |
| | | }, { timeout: 2000 }); |
| | | } else { |
| | | setTimeout(() => { |
| | | this.initTaxonomies(); |
| | | this.initStore(); |
| | | this.initListeners(); |
| | | this.initGallery(); |
| | | }, 100); |
| | | } |
| | | } |
| | | |
| | | initElements() { |
| | | this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons |
| | | this.taxonomyFilters = {}; |
| | | this.elements = { |
| | | filterTrigger: '[data-filter]', |
| | | filters: { |
| | | content: '[data-filter="content"]', |
| | | orderby: '[data-filter="orderby"]', |
| | | order: '[data-filter="order"]', |
| | | match: '[data-filter="match"]', |
| | | favourites: '[data-filter="favourites"]', |
| | | taxonomy: '[data-filter^="taxonomy"]' |
| | | }, |
| | | selectedTax: '.selected-items', |
| | | clearFilter: 'button.clear-filters', |
| | | loadMore: 'button.load-more', |
| | | filterContainer: '.filters', |
| | | grid: '.item-grid', |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.elements); |
| | | |
| | | |
| | | this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false; |
| | | this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]'); |
| | | if (this.ui.content && this.ui.content.length > 0) { |
| | | this.contentTypes = Array.from( |
| | | this.ui.content |
| | | ).map(content => content.value); |
| | | } else { |
| | | this.contentTypes = [this.container.dataset['content']]; |
| | | } |
| | | |
| | | if (this.ui.taxonomies.length>0) { |
| | | this.taxonomies = Array.from( |
| | | this.ui.taxonomies, |
| | | ).map(content => content.dataset.taxonomy); |
| | | } else { |
| | | this.taxonomies = []; |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | async initTaxonomies() { |
| | | this.selector = window.jvbSelector; |
| | | const buttons = document.querySelectorAll('[data-filter="taxonomy"]'); |
| | | |
| | | this.selector.isInitializing = true; |
| | | buttons.forEach((button) => { |
| | | const taxonomy = button.dataset.taxonomy; |
| | | this.currentTaxonomies.add(taxonomy); |
| | | |
| | | this.selector.registerFilterButton(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax |
| | | }); |
| | | |
| | | // Add preload listeners |
| | | this.addTaxonomyPreloadListeners(button, taxonomy); |
| | | }); |
| | | |
| | | this.selector.isInitializing = false; |
| | | |
| | | this.selector.subscribe((event, data) => { |
| | | if (event === 'selected-terms') this.handleTaxonomyChange(data); |
| | | }); |
| | | } |
| | | |
| | | addTaxonomyPreloadListeners(button, taxonomy) { |
| | | const preload = () => { |
| | | this.selector.preloadTaxonomy(taxonomy); |
| | | }; |
| | | |
| | | // Desktop hover |
| | | button.addEventListener('mouseenter', preload, { once: true }); |
| | | |
| | | // Touch/keyboard (fires before click) |
| | | button.addEventListener('pointerdown', preload, { once: true }); |
| | | |
| | | // Keyboard focus |
| | | button.addEventListener('focus', preload, { once: true }); |
| | | } |
| | | |
| | | handleTaxonomyChange(data) { |
| | | const { terms, taxonomy } = data; |
| | | |
| | | // Update only the current taxonomy's terms |
| | | if (terms.size > 0) { |
| | | this.taxonomyFilters[taxonomy] = Array.from(terms.keys()); |
| | | } else { |
| | | // Remove taxonomy if no terms selected |
| | | delete this.taxonomyFilters[taxonomy]; |
| | | } |
| | | |
| | | // Build filters object with all taxonomies |
| | | let filters = { |
| | | page: 1 |
| | | }; |
| | | |
| | | // Add taxonomy filters if any exist |
| | | if (Object.keys(this.taxonomyFilters).length > 0) { |
| | | filters.taxonomy = this.taxonomyFilters; |
| | | } |
| | | |
| | | this.updateFilter(filters); |
| | | } |
| | | |
| | | clearAllTaxonomies() { |
| | | this.taxonomyFilters = {}; |
| | | window.removeChildren(this.ui.selectedTax); |
| | | |
| | | this.updateFilter({ |
| | | taxonomy: null, |
| | | page: 1 |
| | | }); |
| | | } |
| | | |
| | | initFilters() { |
| | | //defaults |
| | | this.filters = { |
| | | content: this.contentTypes[0], |
| | | orderby: 'date', |
| | | order: 'desc', |
| | | page: 1 |
| | | }; |
| | | if (this.config.context) this.filters.context = this.config.context; |
| | | if (this.config.source) this.filters.source = this.config.source; |
| | | |
| | | //check the cache |
| | | this.processCachedFilters(); |
| | | //check url |
| | | this.processURLFilters(); |
| | | |
| | | // Set initial UI state |
| | | this.syncUIToFilters(); |
| | | } |
| | | syncUIToFilters() { |
| | | if (this.ui.filterContainer) { |
| | | // Check radio buttons |
| | | Object.entries(this.filters).forEach(([key, value]) => { |
| | | const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`); |
| | | if (input) { |
| | | input.checked = true; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Update content-specific visibility |
| | | this.updateContentFor(this.filters.content); |
| | | } |
| | | nextPage() { |
| | | this.store.setFilter('page', this.store.filters.page++); |
| | | } |
| | | |
| | | initStore() { |
| | | const store = window.jvbStore.register( |
| | | 'feed', |
| | | { |
| | | storeName: 'feed', |
| | | endpoint: 'feed', |
| | | keyPath: 'id', |
| | | indexes: [ |
| | | { name: 'content', keyPath: 'content'}, |
| | | { name: 'taxonomy', keyPath: 'taxonomy'}, |
| | | { name: 'user', keyPath: 'user'}, |
| | | { name: 'date', keyPath: 'modified'}, |
| | | { name: 'title', keyPath: 'title'} |
| | | ], |
| | | filters: this.filters, |
| | | TTL: 6 * 60 * 60 * 1000, |
| | | showLoading: true, |
| | | required: 'content', |
| | | delayFetch: true |
| | | } |
| | | ); |
| | | this.store = store.feed; |
| | | |
| | | this.store.subscribe((event, data) => { |
| | | switch (event) { |
| | | case 'data-loaded': |
| | | this.renderItems(); |
| | | this.ui.loadMore.hidden = true; |
| | | if (this.store.lastResponse && this.store.lastResponse['has_more']) { |
| | | this.ui.loadMore.hidden = !this.store.lastResponse['has_more']; |
| | | } |
| | | break; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | initGallery() { |
| | | this.gallery = (this.config.gallery) ? window.jvbGallery : false; |
| | | if (this.gallery) { |
| | | this.gallery.subscribe((event, data) => { |
| | | if (event === 'load-more' && this.store.lastResponse) { |
| | | if (this.store.lastResponse['has_more']) { |
| | | this.nextPage(); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | processCachedFilters() { |
| | | Object.keys(this.filters).forEach(filter => { |
| | | let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`); |
| | | if (cached && cached !== this.filters[filter]){ |
| | | this.filters[filter] = cached; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | processURLFilters() { |
| | | if (this.filters.page > 1) { |
| | | return false; |
| | | } |
| | | const params = new URLSearchParams(window.location.search); |
| | | |
| | | if (!params.toString()) { |
| | | return false; |
| | | } |
| | | let filters = ['content', 'order', 'orderby', 'favourites', 'match']; |
| | | filters.forEach(filter => { |
| | | let value = params.get(`f_${filter}`); |
| | | if (value) { |
| | | this.filters[filter] = value; |
| | | let input = this.ui.filters[filter]; |
| | | if (input) { |
| | | input.checked = true; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | let hasTaxonomy = false; |
| | | // Load taxonomy filters from URL |
| | | params.forEach((value, key) => { |
| | | if (key.startsWith('f_tax_')) { |
| | | hasTaxonomy = true; |
| | | const taxonomy = key.replace('f_tax_', ''); |
| | | if (!this.taxonomyFilters[taxonomy]) { |
| | | this.taxonomyFilters[taxonomy] = []; |
| | | } |
| | | this.taxonomyFilters[taxonomy] = value.split(',').map(Number); |
| | | } |
| | | }); |
| | | if (this.ui.filterContainer && hasTaxonomy) { |
| | | for (let [tax, ids] in Object.entries(this.taxonomyFilters)) { |
| | | let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`); |
| | | if (button) { |
| | | if (button.dataset.fieldId) { |
| | | let field = this.selector.get(button.dataset.fieldId); |
| | | field.selectedTerms = new Set(ids); |
| | | this.selector.initFieldDisplay(button.dataset.fieldId); |
| | | } else { |
| | | this.selector.registerField(button, { |
| | | button: button, |
| | | buttonSelector: '[data-filter="taxonomy"]', |
| | | selected: this.ui.selectedTax, |
| | | selectedItems: ids |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Update URL with current filters (for sharing/bookmarking) |
| | | */ |
| | | updateURL() { |
| | | const params = new URLSearchParams(); |
| | | |
| | | // Add simple filters |
| | | ['content', 'order', 'orderby', 'match'].forEach(key => { |
| | | if (this.filters[key]) { |
| | | params.set(`f_${key}`, this.filters[key]); |
| | | } |
| | | }); |
| | | |
| | | // Add taxonomy filters |
| | | Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => { |
| | | if (terms.length > 0) { |
| | | params.set(`f_tax_${taxonomy}`, terms.join(',')); |
| | | } |
| | | }); |
| | | |
| | | // Update URL without reload |
| | | const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`; |
| | | window.history.pushState({ filters: this.filters }, '', newURL); |
| | | } |
| | | |
| | | renderItems() { |
| | | let items = this.store.getFiltered(); |
| | | if (this.store.filters['page'] === 1) { |
| | | window.removeChildren(this.ui.grid); |
| | | } |
| | | |
| | | if (items.length === 0) { |
| | | this.a11y.announceItems(0, this.store.filters['page'] > 0); |
| | | return; |
| | | } |
| | | |
| | | const fragment = document.createDocumentFragment(); |
| | | const batchSize = 10; |
| | | |
| | | const processBatch = (startIndex) => { |
| | | const endIndex = Math.min(startIndex + batchSize, items.length); |
| | | |
| | | for (let i = startIndex; i < endIndex; i++) { |
| | | const item = items[i]; |
| | | const element = this.createItemElement(item); |
| | | |
| | | fragment.appendChild(element); |
| | | } |
| | | |
| | | if (endIndex < items.length) { |
| | | requestAnimationFrame(() => processBatch(endIndex)); |
| | | } else { |
| | | this.removePlaceholders(); |
| | | this.ui.grid.append(fragment); |
| | | |
| | | if (this.config.gallery) { |
| | | this.gallery.updateGalleryItems(this.gallery.getGalleryItems()); |
| | | } |
| | | |
| | | this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])')); |
| | | this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore); |
| | | } |
| | | }; |
| | | |
| | | if (items.length > 0) { |
| | | processBatch(0); |
| | | } else { |
| | | this.a11y.announceItems(0, this.store.filters['page'] >1, false); |
| | | } |
| | | |
| | | if (this.ui.filters.match) { |
| | | this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | if (this.ui.clearFilter) { |
| | | this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * |
| | | * @param {object} item |
| | | */ |
| | | createItemElement(item) { |
| | | let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`); |
| | | |
| | | const isTimeline = Object.hasOwn(template.dataset, 'timeline'); |
| | | |
| | | // Format fields using helpers |
| | | for (let [fieldName, value] of Object.entries(item.fields)) { |
| | | if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue; |
| | | let el = template.querySelector(`[data-field="${fieldName}"]`); |
| | | if (!el) continue; |
| | | |
| | | if (value === '') { |
| | | el.remove(); |
| | | continue; |
| | | } |
| | | |
| | | if (this.isImageField(item, value)) { |
| | | this.formatImageFields(el, value, item); |
| | | } else if (this.isTaxonomyField(item, fieldName)) { |
| | | this.formatTaxonomyField(el, item, fieldName, value); |
| | | } else if (this.isTimeField(el)) { |
| | | this.formatTimeField(el, value); |
| | | } else { |
| | | this.formatField(el, value); |
| | | } |
| | | } |
| | | |
| | | // Handle link |
| | | let link = template.querySelector('a'); |
| | | if (link && item.url !== '') { |
| | | [ |
| | | link.href, |
| | | link.title |
| | | ] = [ |
| | | item.url, |
| | | `View ${item.fields['post_title']??'Item'}` |
| | | ]; |
| | | } |
| | | |
| | | if (isTimeline) { |
| | | this.addTimelineElements(item, template); |
| | | } |
| | | |
| | | return template; |
| | | } |
| | | splitIDs(value) { |
| | | return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value); |
| | | } |
| | | isImageField(item, value) { |
| | | if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) { |
| | | return false; |
| | | } |
| | | let values = this.splitIDs(value); |
| | | |
| | | return values.some(v => |
| | | Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v)) |
| | | ); |
| | | } |
| | | formatImageFields(element, value, item) { |
| | | let values = this.splitIDs(value); // Convert string to array first |
| | | if (values.length === 0) return; |
| | | |
| | | if (values.length > 1) { |
| | | let image = element.querySelector('img'); |
| | | if (!image) return; |
| | | values.forEach(imgID => { |
| | | let img = image.cloneNode(true); |
| | | this.formatImageField(img, imgID, item); |
| | | element.append(img); |
| | | }); |
| | | image.remove(); |
| | | } else { |
| | | if (element.tagName !== 'IMG') { |
| | | element = element.querySelector('img'); |
| | | if (!element) return; |
| | | } |
| | | this.formatImageField(element, values[0], item); |
| | | } |
| | | } |
| | | formatImageField(element, value, item) { |
| | | let imgData = item.images[value]??false; |
| | | if (!imgData) return; |
| | | [ |
| | | element.src, |
| | | element.srcset, |
| | | element.alt |
| | | ] = [ |
| | | imgData.tiny, |
| | | `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`, |
| | | imgData['image-alt-text'] |
| | | ] |
| | | } |
| | | isTaxonomyField(item, field) { |
| | | if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) { |
| | | return false; |
| | | } |
| | | |
| | | return Object.keys(item.taxonomies).includes(field); |
| | | } |
| | | formatTaxonomyField(element, item, field, value) { |
| | | if (element.tagName !== 'UL' || !element.querySelector('li')) return; |
| | | let values = this.splitIDs(value); |
| | | if (values.length === 0) { |
| | | element.remove(); |
| | | } |
| | | let listItem = element.querySelector('li'); |
| | | for (let termID of values) { |
| | | let term = item.taxonomies[field][termID]??false; |
| | | if (!term) continue; |
| | | let termItem = listItem.cloneNode(true); |
| | | let link = termItem.querySelector('a'); |
| | | if (!link) continue; |
| | | |
| | | [ |
| | | link.href, |
| | | link.title, |
| | | link.textContent |
| | | ] = [ |
| | | term.url, |
| | | `See more ${term.title}`, |
| | | term.title |
| | | ]; |
| | | element.append(termItem); |
| | | } |
| | | listItem.remove(); |
| | | } |
| | | isTimeField(el) { |
| | | return el.tagName === 'TIME' || el.querySelector('time') !== null; |
| | | } |
| | | formatTimeField(element, value) { |
| | | if (element.tagName !== 'TIME') { |
| | | element = element.querySelector('time'); |
| | | if (!element) return; |
| | | } |
| | | element.setAttribute('datetime', value); |
| | | element.textContent = window.formatTimeAgo(value, 'F Y'); |
| | | } |
| | | formatField(element, value) { |
| | | element.textContent = value; |
| | | } |
| | | |
| | | addTimelineElements(item, template) { |
| | | let [ |
| | | afterEl, |
| | | number, |
| | | started, |
| | | last |
| | | ] = [ |
| | | template.querySelector('span.after-text'), |
| | | template.querySelector('[data-field="number"] b'), |
| | | template.querySelector('[data-field="started"] time'), |
| | | template.querySelector('[data-field="updated"] time') |
| | | ]; |
| | | |
| | | if (afterEl) { |
| | | afterEl.textContent = `After ${item.fields.number} Tx`; |
| | | } |
| | | if (number) { |
| | | number.textContent = item.fields.number; |
| | | } |
| | | if (started) { |
| | | this.formatTimeField(started, item.fields.timeline[0]['post_date']); |
| | | } |
| | | if (last) { |
| | | this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']); |
| | | } |
| | | } |
| | | |
| | | removePlaceholders() { |
| | | const placeholders = this.ui.grid.querySelectorAll('.placeholder'); |
| | | if (placeholders.length > 0) { |
| | | placeholders.forEach(p => p.remove()); |
| | | } |
| | | } |
| | | |
| | | |
| | | addPlaceholders() { |
| | | let total = this.contentTypes.length; |
| | | const fragment = document.createDocumentFragment(); |
| | | for (let i = 0; i < 12; i++) { |
| | | let template = window.getTemplate('placeholderTemplate'); |
| | | |
| | | let rand = Math.floor(Math.random() * total); |
| | | let icon; |
| | | if (this.ui.content && this.ui.content.length > 0) { |
| | | icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true); |
| | | } else { |
| | | icon = window.getIcon(this.container.dataset.icon); |
| | | } |
| | | template.append(icon); |
| | | fragment.append(template); |
| | | } |
| | | this.ui.grid.append(fragment); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * |
| | | * @param {object} filters {name: value} |
| | | */ |
| | | updateFilter(filters) { |
| | | //double check filters are what we're expecting |
| | | let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)]; |
| | | |
| | | filters = Object.keys(filters) |
| | | .filter(key => allowed.includes(key)) |
| | | .reduce((obj, key) => { |
| | | obj[key] = filters[key]; |
| | | return obj; |
| | | }, {}); |
| | | |
| | | if (window.getDifferences.map(this.filters, filters)) { |
| | | this.filters = { ...this.filters, ...filters }; // Merge instead of replace |
| | | this.updateURL(); |
| | | this.store.setFilters(filters); |
| | | } |
| | | } |
| | | /** |
| | | * Update visible filters based on selected content type |
| | | */ |
| | | updateContentFor(contentType) { |
| | | // Update taxonomy filter visibility |
| | | const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]'); |
| | | taxonomyButtons.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',') || []; |
| | | button.hidden = forTypes.length > 0 && !forTypes.includes(contentType); |
| | | }); |
| | | |
| | | // Update ordering options |
| | | const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]'); |
| | | orderButtons.forEach(button => { |
| | | const forTypes = button.dataset.for?.split(',') || []; |
| | | if (forTypes.length > 0) { |
| | | button.hidden = !forTypes.includes(contentType); |
| | | // Uncheck if hiding |
| | | if (button.hidden && button.checked) { |
| | | button.checked = false; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // Update order direction visibility based on selected orderby |
| | | const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked'); |
| | | this.updateOrderDirectionVisibility(orderBy?.value); |
| | | } |
| | | |
| | | /** |
| | | * Show/hide order direction based on orderby selection |
| | | */ |
| | | updateOrderDirectionVisibility(orderBy) { |
| | | const orderDirection = this.ui.filterContainer.querySelector('.order-direction'); |
| | | if (orderDirection) { |
| | | const forOrders = orderDirection.dataset.forOrder?.split(',') || []; |
| | | orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy); |
| | | } |
| | | } |
| | | /********************************************************************* |
| | | LISTENERS |
| | | *********************************************************************/ |
| | | initListeners() { |
| | | this.popStateHandler = this.handlePopState.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | this.imageObserver = null; |
| | | this.resizeObserver = null; |
| | | if ('IntersectionObserver' in window) { |
| | | this.imageObserver = new IntersectionObserver(entries => { |
| | | entries.forEach(entry => { |
| | | this.loadImage(entry.target); |
| | | this.imageObserver.unobserve(entry.target); |
| | | }); |
| | | }, { |
| | | rootMargin: '100px', |
| | | threshold: .1 |
| | | }); |
| | | } |
| | | |
| | | if ('ResizeObserver' in window) { |
| | | this.resizeObserver = new ResizeObserver(() => { |
| | | window.debouncer.schedule( |
| | | 'feed-update-images', |
| | | () => this.updateImageSizes(), |
| | | 250 |
| | | ); |
| | | }); |
| | | } else { |
| | | window.addEventListener('resize', () => { |
| | | window.debouncer.schedule( |
| | | 'feed-update-images', |
| | | () => this.updateImageSizes(), |
| | | 250 |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | window.addEventListener('popstate', this.popStateHandler); |
| | | document.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('change', this.changeHandler); |
| | | } |
| | | |
| | | handlePopState(e) { |
| | | if (e.state?.filters) { |
| | | if (this.processURLFilters()) { |
| | | this.store.setFilters(this.filters); |
| | | this.a11y.announce('Feed filters updated from browser history'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | handleClick(e) { |
| | | if (window.targetCheck(e, this.elements.loadMore)) { |
| | | this.nextPage(); |
| | | } else if (window.targetCheck(e, this.elements.clearFilter)) { |
| | | this.clearAllTaxonomies(); |
| | | } else if (window.targetCheck(e, '.remove-item')) { |
| | | this.handleRemoveSelectedTerm(e); |
| | | } |
| | | } |
| | | |
| | | handleRemoveSelectedTerm(e) { |
| | | const selectedItem = e.target.closest('.selected-item'); |
| | | if (!selectedItem) return; |
| | | |
| | | const termId = parseInt(selectedItem.dataset.id); |
| | | const taxonomy = selectedItem.dataset.taxonomy; |
| | | |
| | | // Remove from filters |
| | | if (this.taxonomyFilters[taxonomy]) { |
| | | this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy] |
| | | .filter(id => id !== termId); |
| | | |
| | | if (this.taxonomyFilters[taxonomy].length === 0) { |
| | | delete this.taxonomyFilters[taxonomy]; |
| | | } |
| | | } |
| | | |
| | | // Remove from UI |
| | | selectedItem.remove(); |
| | | |
| | | // Update filters |
| | | this.updateFilter({ |
| | | taxonomy: Object.keys(this.taxonomyFilters).length > 0 |
| | | ? this.taxonomyFilters |
| | | : null, |
| | | page: 1 |
| | | }); |
| | | } |
| | | |
| | | handleChange(e) { |
| | | let target = e.target; |
| | | if (Object.hasOwn(target.dataset, 'filter')) { |
| | | if (target.dataset.filter === 'content') { |
| | | this.updateContentFor(target.value); |
| | | this.updateFilter({ content: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'orderby') { |
| | | this.updateOrderDirectionVisibility(target.value); |
| | | this.updateFilter({ orderby: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'order') { |
| | | this.updateFilter({ order: target.value, page: 1 }); |
| | | } else if (target.dataset.filter === 'match') { |
| | | this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 }); |
| | | } else if (target.dataset.filter === 'favourites') { |
| | | this.updateFilter({ favourites: target.checked, page: 1 }); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | window.feedBlock = new FeedBlock(); |
| | | } |
| | | }); |
| | | }); |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/fields", |
| | | "title": "JakeVan Fields", |
| | | "category": "jvb", |
| | | "icon": "ellipses", |
| | | "description": "Access data from your custom fields", |
| | | "keywords": [ "field", "custom", "jake" ], |
| | | "version": "0.9.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "selectors": { |
| | | "root": ".jvb-f" |
| | | }, |
| | | "styles": [], |
| | | "render": "file:./render.php", |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * WordPress dependencies |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { SelectControl, ToggleControl, PanelBody } from '@wordpress/components'; |
| | | |
| | | /** |
| | | * Styles |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * Edit function for Summary Block |
| | | */ |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const blockProps = useBlockProps(); |
| | | |
| | | return ( |
| | | <div {...blockProps}> |
| | | <div className="jvb-summary-preview"> |
| | | <h3>{__('Summary', 'jvb')}</h3> |
| | | <p className="jvb-list-preview-note"> |
| | | {__('This will inherit the current query to build the information from our custom meta on the front end.', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * Directory List Block Editor Styles |
| | | */ |
| | | .jvb-summary-preview { |
| | | padding: 20px; |
| | | background-color: #f8f9fa; |
| | | border: 1px solid #e2e4e7; |
| | | border-radius: 4px; |
| | | |
| | | h3 { |
| | | margin-top: 0; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #ff0080; |
| | | } |
| | | &-note { |
| | | font-style: italic; |
| | | color: #555d66; |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import save from './save'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | |
| | | /** |
| | | * @see ./save.js |
| | | */ |
| | | save, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Render; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Summary Block Render |
| | | * |
| | | * @package Edmonton_Ink |
| | | */ |
| | | |
| | | function jvbRenderSummaryBlock(array $attributes):string |
| | | { |
| | | |
| | | // Buffer output |
| | | if (is_tax()) { |
| | | switch (get_queried_object()->taxonomy) { |
| | | case BASE.'shop': |
| | | return jvbRenderShopSummary(); |
| | | default: |
| | | return jvbRenderTermSummary(); |
| | | } |
| | | } elseif (is_singular()) { |
| | | return jvbRenderArtistSummary(); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function jvbRenderArtistSummary():string |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = Cache::for('artistSummary', WEEK_IN_SECONDS); |
| | | $key = $current->ID; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | $meta = Meta::forPost($current->ID); |
| | | $artist = jvbContentFromUser((int)$current->post_author); |
| | | |
| | | $registrar = Registrar::getInstance($current->post_type); |
| | | $sections = []; |
| | | if ($registrar) { |
| | | $sections = $registrar->getSections(); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | // $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type)); |
| | | ?> |
| | | <nav id="artist" 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> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> |
| | | <li><a href="#about">About</a></li> |
| | | <li><a href="#styles">Styles</a></li> |
| | | <li><a href="#contact">Contact</a></li> |
| | | <li><a href="#work">Work</a></li> |
| | | </ul> |
| | | </nav> |
| | | <header id="top"> |
| | | <h1><small><?=(!empty($artist['city'])) ? $artist['city']['name'] :'Edmonton'?>'s Best <?= (!empty($artist['type'])) ? |
| | | $artist['type']['name']:'Tattoo Artists'?>: |
| | | </small><?=$artist['display_name']?></h1> |
| | | <div> |
| | | <?php if (!empty($artist['shop'])) : ?> |
| | | <ul class="term-list shop"> |
| | | <li> |
| | | <a href="<?=$artist['shop']['url']?>" title="Learn more about <?=$artist['shop']['name']?>"> |
| | | <?= strtolower($artist['shop']['name'])?> |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | <?php endif; ?> |
| | | <?php if (!empty($artist['city'])): ?> |
| | | <ul class="term-list city"> |
| | | <li> |
| | | <a href="<?=$artist['city']['url']?>" title="See who else is rocking out of <?=$artist['city']['name']?>"> |
| | | <?= strtolower($artist['city']['name'])?> |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | <?php endif; ?> |
| | | <?php $styles = $meta->get('top_styles'); |
| | | if (!empty($styles)) { |
| | | ?> |
| | | <ul class="term-list style"> |
| | | <?php |
| | | foreach ($styles as $style) { |
| | | $term = get_term((int)$style, BASE.'style'); |
| | | if ($term && !is_wp_error($term)) { |
| | | $link = get_term_link((int)$style, BASE.'style'); |
| | | ?> |
| | | <li> |
| | | <a href="<?=$link?>" title="Learn more about <?=html_entity_decode($term->name)?>"> |
| | | <?=strtolower(html_entity_decode($term->name))?> |
| | | </a> |
| | | </li> |
| | | <?php |
| | | } |
| | | } |
| | | ?> |
| | | </ul> |
| | | <?php |
| | | } |
| | | ?> |
| | | </div> |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row x-btw"> |
| | | <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2> |
| | | </summary> |
| | | <div class="columns stack-small"> |
| | | <div class="column"> |
| | | <?= Render::renderFrom($meta, 'image_portrait'); ?> |
| | | </div> |
| | | <div class="column"> |
| | | <?= Render::renderFrom($meta, 'short_bio'); ?> |
| | | </div> |
| | | </div> |
| | | <div id="styles"> |
| | | <h3>Works In</h3> |
| | | <?= jvbGetTheTerms('style', $current->ID) ?> |
| | | </div> |
| | | <div class="contact"> |
| | | <h3>Contact:</h3> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->ID, $meta); |
| | | echo jvbRenderLinks($current->ID, $meta); |
| | | ?> |
| | | </div> |
| | | |
| | | <div id="about"> |
| | | <?= Render::renderFrom($meta, 'bio')?> |
| | | </div> |
| | | </details> |
| | | </section> |
| | | <section id="contact" class=""> |
| | | <h2>Contact <?=$artist['name']?></h2> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->ID, 'post'); |
| | | echo jvbRenderLinks($current->ID, 'post'); |
| | | ?> |
| | | </section> |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| | | |
| | | function jvbRenderShopSummary() |
| | | { |
| | | $current = get_queried_object(); |
| | | |
| | | $cache = Cache::for('shop_bio', WEEK_IN_SECONDS)->connect('taxonomy'); |
| | | $key = $current->term_id; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | |
| | | $meta = Meta::forTerm($current->term_id); |
| | | $fields = $meta->getAll(['average_rating', 'established', 'bio','location','hours','specialties','awards','reviews']); |
| | | ?> |
| | | <nav id="shop" 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> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php |
| | | if ($fields['rating'] !== 'none') { |
| | | ?> |
| | | <li><a href="#rating">Rating</a></li> |
| | | <?php |
| | | } elseif ($fields['opened'] !== '') { |
| | | ?> |
| | | <li><a href="#opened">Opened</a></li> |
| | | <?php |
| | | } elseif ($fields['location'] !== '') { |
| | | ?> |
| | | <li><a href="#location">Location</a></li> |
| | | <?php |
| | | } elseif ($fields['about'] !== '') { |
| | | ?> |
| | | <li><a href="#about">About</a></li> |
| | | <?php |
| | | } elseif ($fields['hours'] !== '') { |
| | | ?> |
| | | <li><a href="#hours">Hours</a></li> |
| | | <?php |
| | | } elseif ($fields['specialties'] !== '') { |
| | | ?> |
| | | <li><a href="#specialties">Specialties</a></li> |
| | | <?php |
| | | } elseif ($fields['awards'] !== '') { |
| | | ?> |
| | | <li><a href="#awards">Awards</a></li> |
| | | <?php |
| | | } elseif ($fields['reviews'] !== '') { |
| | | ?> |
| | | <li><a href="#reviews">Reviews</a></li> |
| | | <?php |
| | | } |
| | | ?> |
| | | <li><a href="#contact">Contact</a></li> |
| | | <li><a href="#artists">Artists</a></li> |
| | | </ul> |
| | | </nav> |
| | | <header id="top"> |
| | | <div class="columns stack-small"> |
| | | <div class="column"> |
| | | <?=jvbFormatImage($meta->get('image'))?> |
| | | </div> |
| | | <div class="column"> |
| | | <h1> |
| | | <small><?= (get_term((int)$meta->get('city'), BASE.'city')) ? |
| | | get_term((int)$meta->get('city'), BASE.'city')->name : |
| | | 'Edmonton'?>'s Best Tattoo Shops</small> |
| | | <?=$current->name?> |
| | | </h1> |
| | | <?= jvbFormatRating($current->term_id, 'term') ?> |
| | | <?= Render::renderFrom($meta, 'slogan'); ?> |
| | | </div> |
| | | </div> |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row x-btw"> |
| | | <h2>Learn More About <?=$current->name?></h2> |
| | | </summary> |
| | | <div class="map"> |
| | | <?= Render::renderFrom($meta, 'location'); ?> |
| | | </div> |
| | | <div class="short-bio"> |
| | | <?= Render::renderFrom($meta, 'short_bio'); ?> |
| | | </div> |
| | | |
| | | <div class="contact"> |
| | | <h3>Contact:</h3> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->term_id, 'term'); |
| | | echo jvbRenderLinks($current->term_id, 'term'); |
| | | ?> |
| | | </div> |
| | | |
| | | <div id="about"> |
| | | <?= Render::renderFrom($meta, 'bio')?> |
| | | </div> |
| | | </details> |
| | | </section> |
| | | <section id="contact" class=""> |
| | | <h2>Contact </h2> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->term_id, 'term'); |
| | | echo jvbRenderLinks($current->term_id, 'term'); |
| | | ?> |
| | | </section> |
| | | <?= jvbRenderHours($current->term_id, 'term')?> |
| | | |
| | | |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| | | |
| | | |
| | | function jvbRenderTermSummary() |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = Cache::for('term_summary', WEEK_IN_SECONDS)->connect('taxonomy'); |
| | | $key = $current->ID; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | $tax = jvbNoBase($current->taxonomy); |
| | | switch ($tax) { |
| | | case 'style': |
| | | $title = 'Tattoo Artists'; |
| | | break; |
| | | case 'theme': |
| | | $title = 'Tattoos'; |
| | | break; |
| | | default: |
| | | $title = ''; |
| | | } |
| | | |
| | | $meta = Meta::forTerm($current->ID); |
| | | $fields = $meta->getAll(); |
| | | |
| | | ?> |
| | | <header id="top"> |
| | | <h1><?= get_the_archive_title() ?></h1> |
| | | </header> |
| | | |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| New file |
| | |
| | | export default function save() { |
| | | return null; // Dynamic block rendered by PHP |
| | | } |
| New file |
| | |
| | | details > div { |
| | | margin: 1rem 0; |
| | | } |
| | | |
| | | main > header:not(:has(img)) { |
| | | margin-top: 3rem!important; |
| | | } |
| | | |
| | | header a::before { |
| | | display: none!important; |
| | | } |
| | | |
| | | header + details { |
| | | margin: 1.5rem auto 3rem!important; |
| | | max-width: var(--wide); |
| | | } |
| | | |
| | | main { |
| | | padding-top: 0!important; |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/forms", |
| | | "title": "Contact Forms", |
| | | "category": "jvb", |
| | | "icon": "align-center", |
| | | "description": "Our custom contact forms", |
| | | "keywords": [ "form", "forms", "contact" ], |
| | | "version": "1.0.0", |
| | | "textdomain": "jvb", |
| | | "attributes": { |
| | | "formType": { |
| | | "type": "string", |
| | | "default": "" |
| | | }, |
| | | "showLabels": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "customEmailTo": { |
| | | "type": "string", |
| | | "default": "" |
| | | } |
| | | }, |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "selectors": { |
| | | "root": ".jvb-form-block" |
| | | }, |
| | | "styles": [ |
| | | { "name": "default", "label": "Default", "isDefault": true } |
| | | ], |
| | | "example": { |
| | | "attributes": { |
| | | "formType": "contact", |
| | | "showLabels": true |
| | | } |
| | | }, |
| | | "render": "file:./render.php", |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./editor.scss", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * edit.js |
| | | * WordPress dependencies |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { |
| | | PanelBody, |
| | | SelectControl, |
| | | ToggleControl, |
| | | TextControl, |
| | | Notice |
| | | } from '@wordpress/components'; |
| | | import { useState, useEffect } from '@wordpress/element'; |
| | | |
| | | /** |
| | | * Styles |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * Edit function for Form Block |
| | | */ |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const { |
| | | formType, |
| | | showLabels, |
| | | customEmailTo, |
| | | turnstileEnabled |
| | | } = attributes; |
| | | |
| | | const [isPreviewVisible, setIsPreviewVisible] = useState(true); |
| | | |
| | | const blockProps = useBlockProps({ |
| | | className: `jvb-form-block ${formType ? `jvb-form-block-${formType}` : ''}` |
| | | }); |
| | | |
| | | // Get form types from localized data, with fallback |
| | | const getFormTypes = () => { |
| | | if (typeof window !== 'undefined' && window.jvbFormsData && window.jvbFormsData.formTypes) { |
| | | return window.jvbFormsData.formTypes; |
| | | } |
| | | |
| | | // Fallback if data isn't available |
| | | return [ |
| | | { label: __('Select a form type', 'jvb'), value: '' }, |
| | | { label: __('No forms available', 'jvb'), value: '', disabled: true } |
| | | ]; |
| | | }; |
| | | |
| | | // Get available forms configuration |
| | | const getAvailableForms = () => { |
| | | console.log(window.jvbFormsData); |
| | | if (typeof window !== 'undefined' && window.jvbFormsData && window.jvbFormsData.availableForms) { |
| | | return window.jvbFormsData.availableForms; |
| | | } |
| | | return {}; |
| | | }; |
| | | |
| | | const formTypes = getFormTypes(); |
| | | const availableForms = getAvailableForms(); |
| | | |
| | | // Get form configuration based on selected type |
| | | const getCurrentFormConfig = () => { |
| | | if (!formType || !availableForms[formType]) { |
| | | return null; |
| | | } |
| | | return availableForms[formType]; |
| | | }; |
| | | |
| | | // Form labels based on the selected form type |
| | | const getFormLabels = () => { |
| | | const formConfig = getCurrentFormConfig(); |
| | | |
| | | if (formConfig) { |
| | | return { |
| | | title: formConfig.title || __('Form', 'jvb'), |
| | | description: Array.isArray(formConfig.description) |
| | | ? formConfig.description.join(' ') |
| | | : (formConfig.description || ''), |
| | | button: formConfig.submit || __('Submit', 'jvb') |
| | | }; |
| | | } |
| | | |
| | | return { |
| | | title: __('Form', 'jvb'), |
| | | description: formType ? __('Loading form configuration...', 'jvb') : __('Please select a form type in the sidebar', 'jvb'), |
| | | button: __('Submit', 'jvb') |
| | | }; |
| | | }; |
| | | |
| | | const formLabels = getFormLabels(); |
| | | |
| | | // Render a preview of the form in the editor |
| | | const renderFormPreview = () => { |
| | | if (!formType) { |
| | | return ( |
| | | <Notice status="warning" isDismissible={false}> |
| | | {__('Please select a form type in the block settings.', 'jvb')} |
| | | </Notice> |
| | | ); |
| | | } |
| | | |
| | | let formFields = []; |
| | | |
| | | switch (formType) { |
| | | case 'contact': |
| | | formFields = [ |
| | | { id: 'name', label: __('Name', 'jvb'), type: 'text', required: true }, |
| | | { id: 'email', label: __('Email', 'jvb'), type: 'email', required: true }, |
| | | { id: 'phone', label: __('Phone', 'jvb'), type: 'tel', required: true }, |
| | | { id: 'instagram', label: __('Instagram URL', 'jvb'), type: 'url' }, |
| | | { id: 'contact_methods', label: __('Preferred Contact', 'jvb'), type: 'checkboxes', |
| | | options: [ |
| | | { value: 'text', label: __('Text', 'jvb') }, |
| | | { value: 'call', label: __('Call', 'jvb') }, |
| | | { value: 'email', label: __('Email', 'jvb') }, |
| | | { value: 'instagram', label: __('Instagram', 'jvb') } |
| | | ] |
| | | }, |
| | | { id: 'message', label: __('Your Message', 'jvb'), type: 'textarea', required: true } |
| | | ]; |
| | | break; |
| | | |
| | | case 'feature_request': |
| | | formFields = [ |
| | | { id: 'name', label: __('Name', 'jvb'), type: 'text', help: __('Required if you want us to follow up.', 'jvb') }, |
| | | { id: 'email', label: __('Email', 'jvb'), type: 'email', help: __('Required if you want us to follow up.', 'jvb') }, |
| | | { id: 'follow_up', label: __('Would you like me to follow up with you?', 'jvb'), type: 'checkbox' }, |
| | | { id: 'target_audience', label: __('This Feature is For', 'jvb'), type: 'checkboxes', |
| | | options: [ |
| | | { value: 'artists', label: __('Artists', 'jvb') }, |
| | | { value: 'visitors', label: __('Site Visitors', 'jvb') }, |
| | | { value: 'partners', label: __('Partners', 'jvb') }, |
| | | { value: 'other', label: __('Other', 'jvb') } |
| | | ] |
| | | }, |
| | | { id: 'feature_name', label: __('Name your Feature', 'jvb'), type: 'text', required: true }, |
| | | { id: 'message', label: __('Describe Your Feature', 'jvb'), type: 'textarea', required: true } |
| | | ]; |
| | | break; |
| | | |
| | | case 'technical_issue': |
| | | formFields = [ |
| | | { id: 'name', label: __('Name', 'jvb'), type: 'text' }, |
| | | { id: 'email', label: __('Email', 'jvb'), type: 'email' }, |
| | | { id: 'follow_up', label: __('Would you like me to follow up with you?', 'jvb'), type: 'checkbox' }, |
| | | { id: 'issue_type', label: __('Type of Issue', 'jvb'), type: 'checkboxes', |
| | | options: [ |
| | | { value: 'visual', label: __('Visual', 'jvb') }, |
| | | { value: 'error', label: __('Error Page', 'jvb') }, |
| | | { value: 'other', label: __('Other', 'jvb') } |
| | | ] |
| | | }, |
| | | { id: 'message', label: __('Please describe the issue.', 'jvb'), type: 'textarea', required: true } |
| | | ]; |
| | | break; |
| | | } |
| | | |
| | | return ( |
| | | <> |
| | | <h3 className="jvb-form-title">{formLabels.title}</h3> |
| | | <p className="jvb-form-description">{formLabels.description}</p> |
| | | |
| | | <div className="jvb-form-preview"> |
| | | {formFields.map((field) => ( |
| | | <div key={field.id} className="jvb-form-field"> |
| | | {showLabels && ( |
| | | <label htmlFor={`jvb-${field.id}`} className={field.required ? 'required' : ''}> |
| | | {field.label} |
| | | </label> |
| | | )} |
| | | |
| | | {field.type === 'text' && ( |
| | | <input |
| | | type="text" |
| | | id={`jvb-${field.id}`} |
| | | placeholder={showLabels ? '' : field.label} |
| | | disabled |
| | | /> |
| | | )} |
| | | |
| | | {field.type === 'email' && ( |
| | | <input |
| | | type="email" |
| | | id={`jvb-${field.id}`} |
| | | placeholder={showLabels ? '' : field.label} |
| | | disabled |
| | | /> |
| | | )} |
| | | |
| | | {field.type === 'tel' && ( |
| | | <input |
| | | type="tel" |
| | | id={`jvb-${field.id}`} |
| | | placeholder={showLabels ? '' : field.label} |
| | | disabled |
| | | /> |
| | | )} |
| | | |
| | | {field.type === 'url' && ( |
| | | <input |
| | | type="url" |
| | | id={`jvb-${field.id}`} |
| | | placeholder={showLabels ? '' : field.label} |
| | | disabled |
| | | /> |
| | | )} |
| | | |
| | | {field.type === 'textarea' && ( |
| | | <textarea |
| | | id={`jvb-${field.id}`} |
| | | placeholder={showLabels ? '' : field.label} |
| | | disabled |
| | | rows="4" |
| | | ></textarea> |
| | | )} |
| | | |
| | | {field.type === 'checkbox' && ( |
| | | <div className="jvb-form-checkbox"> |
| | | <input |
| | | type="checkbox" |
| | | id={`jvb-${field.id}`} |
| | | disabled |
| | | /> |
| | | <label htmlFor={`jvb-${field.id}`}>{field.label}</label> |
| | | </div> |
| | | )} |
| | | |
| | | {field.type === 'checkboxes' && field.options && ( |
| | | <div className="jvb-form-checkboxes"> |
| | | {field.options.map((option) => ( |
| | | <div key={option.value} className="jvb-form-checkbox"> |
| | | <input |
| | | type="checkbox" |
| | | id={`jvb-${field.id}-${option.value}`} |
| | | disabled |
| | | /> |
| | | <label htmlFor={`jvb-${field.id}-${option.value}`}>{option.label}</label> |
| | | </div> |
| | | ))} |
| | | </div> |
| | | )} |
| | | |
| | | {field.help && ( |
| | | <p className="jvb-form-help">{field.help}</p> |
| | | )} |
| | | </div> |
| | | ))} |
| | | |
| | | {turnstileEnabled && ( |
| | | <div className="jvb-form-turnstile"> |
| | | <div className="jvb-turnstile-placeholder"> |
| | | <span>{__('Cloudflare Turnstile will appear here', 'jvb')}</span> |
| | | </div> |
| | | </div> |
| | | )} |
| | | |
| | | <div className="jvb-form-submit"> |
| | | <button type="button" className="jvb-form-button">{formLabels.button}</button> |
| | | </div> |
| | | </div> |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('Form Settings', 'jvb')}> |
| | | <SelectControl |
| | | label={__('Form Type', 'jvb')} |
| | | value={formType} |
| | | options={formTypes} |
| | | onChange={(value) => setAttributes({ formType: value })} |
| | | /> |
| | | |
| | | <ToggleControl |
| | | label={__('Show Field Labels', 'jvb')} |
| | | checked={showLabels} |
| | | onChange={(value) => setAttributes({ showLabels: value })} |
| | | help={__('Toggle to show or hide field labels.', 'jvb')} |
| | | /> |
| | | |
| | | <TextControl |
| | | label={__('Custom Recipient Email', 'jvb')} |
| | | value={customEmailTo || ''} |
| | | onChange={(value) => setAttributes({ customEmailTo: value })} |
| | | help={__('Leave empty to use the default email.', 'jvb')} |
| | | type="email" |
| | | /> |
| | | </PanelBody> |
| | | |
| | | <PanelBody title={__('Form Preview', 'jvb')} initialOpen={false}> |
| | | <ToggleControl |
| | | label={__('Show Preview', 'jvb')} |
| | | checked={isPreviewVisible} |
| | | onChange={(value) => setIsPreviewVisible(value)} |
| | | /> |
| | | |
| | | <p className="components-base-control__help"> |
| | | {__('This is just a preview. The actual form will be rendered on the frontend.', 'jvb')} |
| | | </p> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div {...blockProps}> |
| | | {isPreviewVisible ? ( |
| | | renderFormPreview() |
| | | ) : ( |
| | | <div className="jvb-form-placeholder"> |
| | | <h3>{formLabels.title}</h3> |
| | | <p>{__('Form preview is hidden. Edit settings in the sidebar.', 'jvb')}</p> |
| | | </div> |
| | | )} |
| | | </div> |
| | | </> |
| | | ); |
| | | } |
| New file |
| | |
| | | //index.js |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import save from './save'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | |
| | | /** |
| | | * @see ./save.js |
| | | */ |
| | | save, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Form Block Render |
| | | * |
| | | * @package Edmonton_Ink |
| | | */ |
| | | |
| | | /** |
| | | * Renders the form block on the frontend |
| | | * |
| | | * @param array $attributes The block attributes. |
| | | * @param string $content The block content. |
| | | * @param WP_Block $block The block instance. |
| | | * @return string The rendered output. |
| | | */ |
| | | function jvbRenderFormBlock(array $attributes, string $content, WP_Block $block):string |
| | | { |
| | | // Get form type from attributes |
| | | $form_type = isset($attributes['formType']) ? $attributes['formType'] : ''; |
| | | |
| | | // If no form type selected, return a message |
| | | if (empty($form_type)) { |
| | | return '<div class="jvb-form-error">No form type selected. Please edit this block and select a form type.</div>'; |
| | | } |
| | | |
| | | // Get other attributes |
| | | $show_labels = isset($attributes['showLabels']) ? $attributes['showLabels'] : true; |
| | | $custom_email_to = isset($attributes['customEmailTo']) ? $attributes['customEmailTo'] : ''; |
| | | |
| | | // Set custom options for the form |
| | | $form_options = array(); |
| | | |
| | | // Set custom email recipient if provided |
| | | if (!empty($custom_email_to)) { |
| | | $form_options['email_to'] = $custom_email_to; |
| | | } |
| | | |
| | | // Render the form with the specified options |
| | | $form_output = JVB()->forms()->renderForm($form_type); |
| | | |
| | | // Get block classes |
| | | $wrapper_attributes = get_block_wrapper_attributes([ |
| | | 'class' => 'jvb-forms' |
| | | ]); |
| | | |
| | | // Return the wrapped form |
| | | return sprintf( |
| | | '<div %1$s>%2$s</div>', |
| | | $wrapper_attributes, |
| | | $form_output |
| | | ); |
| | | } |
| New file |
| | |
| | | //save.js |
| | | /** |
| | | * React hook that is used to mark the block wrapper element. |
| | | * It provides all the necessary props like the class name. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops |
| | | */ |
| | | import { useBlockProps } from '@wordpress/block-editor'; |
| | | |
| | | /** |
| | | * The save function defines the way in which the different attributes should |
| | | * be combined into the final markup, which is then serialized by the block |
| | | * editor into `post_content`. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save |
| | | * |
| | | * @return {WPElement} Element to render. |
| | | */ |
| | | export default function save() { |
| | | // This is a dynamic block that is rendered on the server side |
| | | // Return null to let WordPress handle the saving and rendering |
| | | return null; |
| | | } |
| New file |
| | |
| | | /** |
| | | * view.js |
| | | * Frontend JavaScript for the Form Block |
| | | * Handles form validation and submission |
| | | */ |
| | | /** |
| | | * view.js |
| | | * Frontend JavaScript for the Form Block |
| | | */ |
| | | class FormBlock { |
| | | constructor() { |
| | | this.controller = window.jvbForm; |
| | | |
| | | document.querySelectorAll('.jvb-form-block form').forEach(form => { |
| | | this.controller.registerForm(form, { |
| | | cache: true, |
| | | autoUpload: false, |
| | | imageMeta: false, |
| | | }); |
| | | }); |
| | | |
| | | this.controller.subscribe((event, data) => { |
| | | if (event === 'form-submit') { |
| | | this.handleFormSubmission(data).then(()=>{}); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async handleFormSubmission(eventData) { |
| | | const { config, data } = eventData; |
| | | const form = config.element; |
| | | |
| | | const submitData = new FormData(); |
| | | |
| | | // Add regular form fields |
| | | for (const [key, value] of Object.entries(data)) { |
| | | if (Array.isArray(value)) { |
| | | value.forEach(v => submitData.append(`${key}[]`, v)); |
| | | } else if (typeof value === 'object' && value !== null) { |
| | | submitData.append(key, JSON.stringify(value)); |
| | | } else { |
| | | submitData.append(key, value); |
| | | } |
| | | } |
| | | config.element.querySelectorAll('[name="form_id"],[name="form_type"],[name="timestamp"],[name="cf-turnstile-response"]').forEach(input => { |
| | | submitData.append(input.name, input.value); |
| | | }); |
| | | |
| | | // Add uploaded files |
| | | if (window.jvbUploads) { |
| | | try { |
| | | const files = await window.jvbUploads.getFilesForForm(form); |
| | | |
| | | files.forEach(({ file, fieldName }) => { |
| | | submitData.append(`${fieldName}[]`, file); |
| | | }); |
| | | } catch (error) { |
| | | console.error('Error getting files:', error); |
| | | } |
| | | } |
| | | |
| | | this.controller.showFormStatus(config.id, 'uploading'); |
| | | |
| | | try { |
| | | const response = await fetch(`${jvbSettings.api}forms`, { |
| | | method: 'POST', |
| | | credentials: 'same-origin', |
| | | body: submitData |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | |
| | | if (!response.ok) { |
| | | this.controller.showFormStatus(config.id, 'error'); |
| | | this.controller.handleFormError(form, result); |
| | | return; |
| | | } |
| | | |
| | | this.controller.showFormStatus(config.id, 'submitted'); |
| | | // this.controller.handleFormSuccess(form, result); |
| | | this.controller.showSummary({ changes: data, config: config }); |
| | | window.jvbA11y.announce('Form successfully submitted!'); |
| | | |
| | | // Clean up uploaded files |
| | | if (window.jvbUploads) { |
| | | const uploadFields = form.querySelectorAll('[data-upload-field]'); |
| | | for (const field of uploadFields) { |
| | | const fieldId = window.jvbUploads.determineFieldId(field); |
| | | await window.jvbUploads.clearFieldFromStores(fieldId); |
| | | } |
| | | } |
| | | |
| | | } catch (error) { |
| | | console.error('Form submission error:', error); |
| | | this.controller.showFormStatus(config.id, 'error'); |
| | | this.controller.handleFormError(form, { |
| | | message: 'Network error. Please check your connection and try again.', |
| | | code: 'network_error' |
| | | }); |
| | | } finally { |
| | | await this.controller.store.delete(config.id); |
| | | } |
| | | } |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function() { |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | new FormBlock(); |
| | | } |
| | | }); |
| | | }); |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/glossary", |
| | | "version": "0.1.0", |
| | | "title": "Glossary of Terms", |
| | | "category": "jvb", |
| | | "icon": "excerpt-view", |
| | | "description": "Outputs the terms", |
| | | "example": {}, |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "textdomain": "jvb", |
| | | "selectors": { |
| | | "root": ".glossary" |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "render": "file:./render.php", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * Retrieves the translation of text. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/ |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | |
| | | /** |
| | | * React hook that is used to mark the block wrapper element. |
| | | * It provides all the necessary props like the class name. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops |
| | | */ |
| | | import { useBlockProps } from '@wordpress/block-editor'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * Those files can contain any CSS code that gets applied to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * The edit function describes the structure of your block in the context of the |
| | | * editor. This represents what the editor will render when the block is used. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit |
| | | * |
| | | * @return {Element} Element to render. |
| | | */ |
| | | export default function Edit() { |
| | | return ( |
| | | <p { ...useBlockProps() }> |
| | | { __( 'Will output the glossary', 'jvb' ) } |
| | | </p> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | | ?> |
| | | <p <?php echo get_block_wrapper_attributes(); ?>> |
| | | <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?> |
| | | </p> |
| New file |
| | |
| | | :root { |
| | | --navWidth: 40vw; |
| | | @media (min-width: 768px) { |
| | | --navWidth: 22vw; |
| | | } |
| | | } |
| | | |
| | | nav.glossary-index { |
| | | position: fixed; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: var(--navWidth); |
| | | right: -8px; |
| | | height: 60vh; |
| | | z-index: var(--z-3); |
| | | |
| | | > ul { |
| | | --dir: column; |
| | | --align: flex-start; |
| | | --justify: flex-start; |
| | | --gap: 1px; |
| | | touch-action: pan-y; |
| | | max-height: 100%; |
| | | height: 100%; |
| | | width: 100%; |
| | | overflow: hidden auto; |
| | | scroll-behavior: smooth; |
| | | } |
| | | li, a { |
| | | flex: 1; |
| | | width: 100%; |
| | | height: max-content; |
| | | min-height: max(var(--chipchip), max-content); |
| | | } |
| | | a { |
| | | --justify: center; |
| | | padding: .25rem .5rem; |
| | | hyphens: auto; |
| | | background-color: rgba(var(--base-rgb),var(--op-45)); |
| | | word-wrap: anywhere; |
| | | white-space: wrap; |
| | | } |
| | | a:hover, |
| | | a:focus, |
| | | a.active { |
| | | background-color: rgba(var(--action-rgb), var(--op-6)); |
| | | color: var(--action-contrast); |
| | | } |
| | | } |
| | | .glossary dd { |
| | | margin-left: .5rem; |
| | | width: calc(100% + .75rem); |
| | | } |
| | | .glossary dd, |
| | | .glossary dt { |
| | | position: relative; |
| | | left: 0; |
| | | transition: margin var(--trans-base), |
| | | left var(--trans-base), |
| | | width var(--trans-base); |
| | | } |
| | | .glossary dt:target, |
| | | .glossary dt.active { |
| | | outline: none; |
| | | left: -1.5rem; |
| | | padding: 0; |
| | | color: var(--action-0); |
| | | } |
| | | .glossary dt:target + dd, |
| | | .glossary dt.active + dd { |
| | | left: -1.5rem; |
| | | } |
| | | |
| | | main header, |
| | | dl.glossary { |
| | | grid-column: full; |
| | | padding: 0 var(--navWidth) 0 2rem; |
| | | @media (min-width:768px) { |
| | | margin-left: auto; |
| | | max-width: var(--content); |
| | | margin-right: var(--navWidth); |
| | | padding-right: var(--btn); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .glossary { |
| | | h2 { |
| | | font-size: var(--txt-medium); |
| | | } |
| | | p { |
| | | font-size: var(--txt-x-small); |
| | | } |
| | | } |
| | | .glossary-index { |
| | | li,a { |
| | | height: fit-content; |
| | | } |
| | | a { |
| | | font-size: var(--txt-x-small); |
| | | padding: .25rem; |
| | | min-height: 2em; |
| | | } |
| | | } |
| | | |
| | | body:has(.glossary) h1 { |
| | | font-size: var(--txt-xx-large); |
| | | } |
| | | } |
| New file |
| | |
| | | /** |
| | | * Glossary Navigation Active State Manager |
| | | * Handles highlighting active terms as they scroll into view |
| | | * and syncing navigation with scroll position |
| | | */ |
| | | class GlossaryNavigator { |
| | | constructor(glossarySelector = 'dl.glossary', navSelector = 'nav.glossary-index') { |
| | | this.glossary = document.querySelector(glossarySelector); |
| | | this.nav = document.querySelector(navSelector); |
| | | |
| | | if (!this.glossary || !this.nav) return; |
| | | |
| | | this.terms = this.glossary.querySelectorAll('dt[id]'); |
| | | this.navList = this.nav.querySelector('ul'); |
| | | this.activeClass = 'active'; |
| | | this.currentActive = null; |
| | | |
| | | this.init(); |
| | | this.setupResizeHandler(); |
| | | } |
| | | |
| | | init() { |
| | | // Set up Intersection Observer with screen-size appropriate margins |
| | | const observerOptions = { |
| | | root: null, // viewport |
| | | rootMargin: this.getRootMargin(), |
| | | threshold: 0 |
| | | }; |
| | | |
| | | this.observer = new IntersectionObserver( |
| | | (entries) => this.handleIntersection(entries), |
| | | observerOptions |
| | | ); |
| | | |
| | | // Observe all terms |
| | | this.terms.forEach(term => this.observer.observe(term)); |
| | | |
| | | // Also handle manual scroll for edge cases |
| | | this.handleScroll = this.debounce(() => this.checkActiveTerm(), 100); |
| | | window.addEventListener('scroll', this.handleScroll, { passive: true }); |
| | | } |
| | | |
| | | getRootMargin() { |
| | | // On larger screens: centered (50% from top and bottom) |
| | | return '-50% 0px -50% 0px'; |
| | | } |
| | | |
| | | setupResizeHandler() { |
| | | let resizeTimer; |
| | | window.addEventListener('resize', () => { |
| | | clearTimeout(resizeTimer); |
| | | resizeTimer = setTimeout(() => { |
| | | // Reinitialize observer with new margins on resize |
| | | this.reinitialize(); |
| | | }, 250); |
| | | }); |
| | | } |
| | | |
| | | reinitialize() { |
| | | // Disconnect old observer |
| | | if (this.observer) { |
| | | this.observer.disconnect(); |
| | | } |
| | | |
| | | // Create new observer with updated margins |
| | | this.init(); |
| | | } |
| | | |
| | | handleIntersection(entries) { |
| | | // Find the entry that's intersecting |
| | | const intersecting = entries.find(entry => entry.isIntersecting); |
| | | |
| | | if (intersecting) { |
| | | this.setActive(intersecting.target); |
| | | } |
| | | } |
| | | |
| | | checkActiveTerm() { |
| | | // Fallback method to find which term is in the trigger zone |
| | | const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize); |
| | | const margin = remInPixels * 4; |
| | | |
| | | let closestTerm = null; |
| | | let closestDistance = Infinity; |
| | | |
| | | this.terms.forEach(term => { |
| | | const rect = term.getBoundingClientRect(); |
| | | |
| | | const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight; |
| | | |
| | | if (isInZone) { |
| | | // Find closest to the trigger point |
| | | const triggerPoint = window.innerHeight / 2; |
| | | |
| | | const distance = Math.abs(rect.top - triggerPoint); |
| | | |
| | | if (distance < closestDistance) { |
| | | closestDistance = distance; |
| | | closestTerm = term; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | if (closestTerm) { |
| | | this.setActive(closestTerm); |
| | | } |
| | | } |
| | | |
| | | setActive(term) { |
| | | if (this.currentActive === term) return; |
| | | |
| | | // Remove active class from previous term |
| | | if (this.currentActive) { |
| | | this.currentActive.classList.remove(this.activeClass); |
| | | } |
| | | |
| | | // Add active class to current term |
| | | term.classList.add(this.activeClass); |
| | | this.currentActive = term; |
| | | |
| | | // Update navigation |
| | | this.updateNavigation(term.id); |
| | | } |
| | | |
| | | updateNavigation(termId) { |
| | | // Remove active from all nav links |
| | | const navLinks = this.nav.querySelectorAll('a'); |
| | | navLinks.forEach(link => link.classList.remove(this.activeClass)); |
| | | |
| | | // Find and activate corresponding nav link |
| | | const activeLink = this.nav.querySelector(`a[href="#${termId}"]`); |
| | | |
| | | if (activeLink) { |
| | | activeLink.classList.add(this.activeClass); |
| | | |
| | | // Scroll the nav list to center the active link |
| | | this.centerNavItem(activeLink); |
| | | } |
| | | } |
| | | |
| | | centerNavItem(link) { |
| | | const listRect = this.navList.getBoundingClientRect(); |
| | | const linkRect = link.getBoundingClientRect(); |
| | | |
| | | // Calculate position to center the link in the nav container |
| | | const scrollTop = this.navList.scrollTop; |
| | | const linkOffset = linkRect.top - listRect.top; |
| | | const centerOffset = (listRect.height / 2) - (linkRect.height / 2); |
| | | |
| | | this.navList.scrollTo({ |
| | | top: scrollTop + linkOffset - centerOffset, |
| | | behavior: 'smooth' |
| | | }); |
| | | } |
| | | |
| | | debounce(func, wait) { |
| | | let timeout; |
| | | return function executedFunction(...args) { |
| | | const later = () => { |
| | | clearTimeout(timeout); |
| | | func(...args); |
| | | }; |
| | | clearTimeout(timeout); |
| | | timeout = setTimeout(later, wait); |
| | | }; |
| | | } |
| | | |
| | | destroy() { |
| | | if (this.observer) { |
| | | this.observer.disconnect(); |
| | | } |
| | | window.removeEventListener('scroll', this.handleScroll); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Initialize when DOM is ready |
| | | if (document.readyState === 'loading') { |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | new GlossaryNavigator(); |
| | | }); |
| | | } else { |
| | | new GlossaryNavigator(); |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/gmbreviews", |
| | | "title": "GMB Reviews", |
| | | "category": "jvb", |
| | | "description": "Display top-rated Google My Business reviews with statistics and action buttons", |
| | | "keywords": ["reviews", "google", "testimonials", "gmb", "ratings"], |
| | | "textdomain": "jvb", |
| | | "attributes": { |
| | | "inheritUser": { |
| | | "type": "boolean", |
| | | "default": false |
| | | }, |
| | | "count": { |
| | | "type": "number", |
| | | "default": 5 |
| | | }, |
| | | "showRating": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "showDate": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "showReviewLink": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "showViewAllLink": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "showStats": { |
| | | "type": "boolean", |
| | | "default": true |
| | | }, |
| | | "minStars": { |
| | | "type": "number", |
| | | "default": 4, |
| | | "minimum": 1, |
| | | "maximum": 5 |
| | | } |
| | | }, |
| | | "supports": { |
| | | "html": false, |
| | | "align": true, |
| | | "color": { |
| | | "text": true, |
| | | "background": true, |
| | | "link": true |
| | | }, |
| | | "spacing": { |
| | | "margin": true, |
| | | "padding": true |
| | | }, |
| | | "typography": { |
| | | "fontSize": true, |
| | | "lineHeight": true |
| | | } |
| | | }, |
| | | "render": "file:./render.php", |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | // src/gmbreviews/edit.js |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components'; |
| | | import { __ } from '@wordpress/i18n'; |
| | | import ServerSideRender from '@wordpress/server-side-render'; |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const blockProps = useBlockProps(); |
| | | const { count, inheritUser, showStats, minStars, showViewAllLink, showRating, showDate, showReviewLink } = attributes; |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('Review Settings', 'jvb')}> |
| | | <ToggleControl |
| | | label={__('Inherit User', 'jvb')} |
| | | checked={inheritUser} |
| | | onChange={(value) => setAttributes({ inheritUser: value })} |
| | | /> |
| | | <RangeControl |
| | | label={__('Number of Reviews', 'jvb')} |
| | | value={count} |
| | | onChange={(value) => setAttributes({ count: value })} |
| | | min={1} |
| | | max={20} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Show Rating', 'jvb')} |
| | | checked={showRating} |
| | | onChange={(value) => setAttributes({ showRating: value })} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Show Date', 'jvb')} |
| | | checked={showDate} |
| | | onChange={(value) => setAttributes({ showDate: value })} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Show Review Link', 'jvb')} |
| | | checked={showReviewLink} |
| | | onChange={(value) => setAttributes({ showReviewLink: value })} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Show Stats', 'jvb')} |
| | | checked={showStats} |
| | | onChange={(value) => setAttributes({ showStats: value })} |
| | | /> |
| | | <ToggleControl |
| | | label={__('Show All Reviews Link', 'jvb')} |
| | | checked={showViewAllLink} |
| | | onChange={(value) => setAttributes({ showViewAllLink: value })} |
| | | /> |
| | | <RangeControl |
| | | label={__('Minimum Rating', 'jvb')} |
| | | value={minStars} |
| | | onChange={(value) => setAttributes({ minStars: value })} |
| | | min={1} |
| | | max={5} |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | <div {...blockProps}> |
| | | <ServerSideRender |
| | | block="jvb/gmbreviews" |
| | | attributes={attributes} |
| | | /> |
| | | </div> |
| | | </> |
| | | ); |
| | | } |
| New file |
| | |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | import Edit from './edit'; |
| | | import './style.scss'; |
| | | import './editor.scss'; |
| | | import metadata from './block.json'; |
| | | |
| | | registerBlockType(metadata.name, { |
| | | edit: Edit, |
| | | // No save function - dynamic block rendered on server |
| | | save: () => null, |
| | | }); |
| New file |
| | |
| | | <?php |
| | | /** |
| | | * GMB Reviews Block - Render Template |
| | | * |
| | | * Displays recent Google My Business reviews with a link to leave a review |
| | | */ |
| | | function jvbRenderGMBReviewsBlock(array $attributes): string |
| | | { |
| | | $count = $attributes['count'] ?? 5; |
| | | $showRating = $attributes['showRating'] ?? true; |
| | | $showDate = $attributes['showDate'] ?? true; |
| | | $showReviewLink = $attributes['showReviewLink'] ?? true; |
| | | $showViewAllLink = $attributes['showViewAllLink'] ?? true; |
| | | $showStats = $attributes['showStats'] ?? true; |
| | | $minStars = $attributes['minStars'] ?? 4; // Only show 4+ star reviews |
| | | $inheritUser = $attributes['inheritUser']??null; |
| | | if ($inheritUser) { |
| | | global $post; |
| | | $inheritUser = $post->post_author; |
| | | }else { |
| | | $inheritUser = null; |
| | | } |
| | | try { |
| | | $gmb = JVB()->connect('gmb', $inheritUser); |
| | | if (!$gmb->isSetUp()) { |
| | | error_log('GMB Not set up for: '.(int)$inheritUser); |
| | | return ''; |
| | | } |
| | | $gotReviews = $gmb->getReviews(); |
| | | // Get all data |
| | | $allReviews = $gotReviews['reviews']??[]; |
| | | $reviewUrl = $gmb->getReviewUrl(); |
| | | $viewAllUrl = $gmb->getReviewsViewUrl(); |
| | | |
| | | $average = $gotReviews['averageRating']??null; |
| | | $total = $gotReviews['totalReviewCount']??null; |
| | | |
| | | // Filter reviews by minimum stars |
| | | $reviews = []; |
| | | if (!empty($allReviews)) { |
| | | foreach ($allReviews as $review) { |
| | | $rating = $review['starRating'] ?? 0; |
| | | if ($rating >= $minStars) { |
| | | $reviews[] = $review; |
| | | if (count($reviews) >= $count) { |
| | | break; // Got enough reviews |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (empty($reviews) && empty($reviewUrl) && empty($stats)) { |
| | | error_log('No reviews to display...'); |
| | | return ''; |
| | | } |
| | | |
| | | ob_start(); |
| | | ?> |
| | | <div class="gmb-reviews"> |
| | | <div class="row center"> |
| | | <?php |
| | | if ($showStats && !empty($average) && !empty($total)) { |
| | | ?> |
| | | <p> |
| | | <span class="stars" title="<?= $average ?> out of 5 stars"> |
| | | <?php |
| | | $fullStars = floor($average); |
| | | $hasHalfStar = ($average - $fullStars) >= 0.5; |
| | | |
| | | for ($i = 1; $i <= 5; $i++) { |
| | | if ($i <= $fullStars) { |
| | | echo jvbIcon('star', ['style' => 'fill']); |
| | | } elseif ($i == $fullStars + 1 && $hasHalfStar) { |
| | | echo jvbIcon('star-half', ['style'=> 'fill']); |
| | | } else { |
| | | echo jvbIcon('star', ['style' => 'light']); |
| | | } |
| | | } |
| | | ?> |
| | | </span> |
| | | <i>Average</i> |
| | | </p> |
| | | <?php |
| | | if ($total > 0) { |
| | | ?> |
| | | <p><i>{ <?= number_format($total ) . ' ' . _n('Review', 'Reviews', $total, 'jvb')?> Total }</i></p> |
| | | <?php |
| | | } |
| | | ?> |
| | | <?php |
| | | } |
| | | ?> |
| | | |
| | | |
| | | </div> |
| | | <?php |
| | | if ($showReviewLink && !empty($reviewUrl)) { |
| | | ?> |
| | | <a href="<?=esc_url($reviewUrl)?>" |
| | | class="button" |
| | | target="_blank" |
| | | rel="noopener noreferrer"> |
| | | <?= jvbIcon('star', ['style' => 'fill']) ?> |
| | | Leave Your Review |
| | | </a> |
| | | <?php |
| | | } |
| | | ?> |
| | | |
| | | <ul> |
| | | <?php |
| | | foreach ($reviews as $review) { |
| | | $reviewer = $review['reviewer']['displayName'] ?? 'Anonymous'; |
| | | $reviewer = strtok($reviewer, ' '); |
| | | $profilePhoto = $review['reviewer']['profilePhotoUrl'] ?? ''; |
| | | $rating = $review['starRating'] ?? 0; |
| | | $rating = match($rating) { |
| | | 'FIVE' => 5, |
| | | 'FOUR' => 4, |
| | | 'THREE' => 3, |
| | | 'TWO' => 2, |
| | | 'ONE' => 1, |
| | | default => $rating |
| | | }; |
| | | $comment = $review['comment'] ?? ''; |
| | | $date = $review['updateTime'] ?? ''; |
| | | ?> |
| | | <li> |
| | | <blockquote class="review"> |
| | | <?php |
| | | // Review text |
| | | if (!empty($comment)) { ?> |
| | | <div class="content review"> |
| | | <?= apply_filters('wpautop', $comment) ?> |
| | | </div> |
| | | <?php } ?> |
| | | <cite class="row left nowrap"> |
| | | <?php if (!empty($profilePhoto)) { ?> |
| | | <img src="<?=esc_url($profilePhoto)?>" |
| | | alt="<?=esc_attr($reviewer)?>" |
| | | 'loading="lazy"> |
| | | <?php } else { ?> |
| | | <div class="avatar"> |
| | | <?= jvbIcon('user-circle')?> |
| | | </div> |
| | | <?php } ?> |
| | | |
| | | <div class="row left wrap"> |
| | | <?php if ($showRating && $rating > 0) { ?> |
| | | <div class="stars" title="<?= $rating ?> out of 5 stars"> |
| | | <?php |
| | | for ($i = 1; $i <= 5; $i++) { |
| | | echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']); |
| | | } ?> |
| | | </div> |
| | | <?php } ?> |
| | | <p><?= esc_html($reviewer)?></p> |
| | | <?php |
| | | // Date |
| | | if ($showDate && !empty($date)) { |
| | | $formatted_date = human_time_diff(strtotime($date), current_time('timestamp')) . ' ago'; |
| | | ?> |
| | | <time datetime="<?=esc_attr($date)?>"> |
| | | <?= esc_html($formatted_date) ?> |
| | | </time> |
| | | <?php } ?> |
| | | |
| | | </div> |
| | | </cite> |
| | | </blockquote> |
| | | </li> |
| | | <?php |
| | | } |
| | | ?> |
| | | </ul> |
| | | <?php |
| | | // Footer with "See All Reviews" button |
| | | if ($showViewAllLink && !empty($viewAllUrl)) { |
| | | ?> |
| | | <div class="footer"> |
| | | <a href=" <?= esc_url($viewAllUrl) ?>" |
| | | class="button" |
| | | target="_blank" |
| | | rel="noopener noreferrer"> |
| | | |
| | | <?php |
| | | if ($showStats ) { |
| | | echo 'See All ' . number_format($total) . ' Reviews'; |
| | | } else { |
| | | echo ' See All Reviews'; |
| | | } |
| | | ?> |
| | | <?= jvbIcon('arrow-square-out') ?> |
| | | </a> |
| | | </div> |
| | | <?php |
| | | } |
| | | ?> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | | |
| | | } catch (\Exception $e) { |
| | | error_log('[GMB Reviews Block] Error: ' . $e->getMessage()); |
| | | return ''; |
| | | } |
| | | } |
| New file |
| | |
| | | .gmb-reviews { |
| | | max-width: none; |
| | | > .row.center { |
| | | max-width:var(--content); |
| | | margin: 0 auto; |
| | | --gap: .5rem 6rem; |
| | | |
| | | p { |
| | | width: fit-content; |
| | | } |
| | | } |
| | | .button { |
| | | width: 66.6%; |
| | | margin: 0 auto 2rem; |
| | | display: flex; |
| | | height: max-content; |
| | | } |
| | | .stars { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | flex-wrap: nowrap; |
| | | } |
| | | ul { |
| | | list-style: none; |
| | | margin: 0; |
| | | padding: 0; |
| | | max-width: var(--full); |
| | | li { |
| | | width: 100%; |
| | | max-width: none; |
| | | padding: 4rem 1rem; |
| | | &:nth-of-type(odd) { |
| | | background-color: var(--base-50); |
| | | blockquote { |
| | | --background: var(--base-50); |
| | | } |
| | | } |
| | | &:nth-of-type(even) { |
| | | background-color: var(--base-100); |
| | | blockquote { |
| | | --background: var(--base-100); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | blockquote { |
| | | margin:0 auto; |
| | | padding: 0; |
| | | max-width: var(--content); |
| | | .content { |
| | | border-width: 4px 1px; |
| | | &::after { |
| | | border-width: 4px 1px; |
| | | } |
| | | &::before { |
| | | border-width: 8px; |
| | | bottom: -4px; |
| | | } |
| | | } |
| | | cite { |
| | | position: relative; |
| | | |
| | | img { |
| | | width: 4.5rem; |
| | | position: absolute; |
| | | left: -8rem; |
| | | top: 0; |
| | | } |
| | | |
| | | p { |
| | | margin: 0; |
| | | } |
| | | |
| | | .wrap { |
| | | --wrap: wrap; |
| | | |
| | | p, time { |
| | | max-width: 49%; |
| | | } |
| | | |
| | | .stars { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | time { |
| | | white-space: nowrap; |
| | | } |
| | | } |
| | | .stars .icon { |
| | | background-color: var(--action-0); |
| | | } |
| | | article { |
| | | padding: 1rem; |
| | | border-radius: var(--radius-outer); |
| | | background-color: var(--base); |
| | | header { |
| | | --align: center; |
| | | >img { |
| | | position: relative; |
| | | left: 0; |
| | | } |
| | | } |
| | | time { |
| | | font-style: italic; |
| | | } |
| | | .review { |
| | | padding: 1.5rem; |
| | | } |
| | | |
| | | h4 { |
| | | width: max-content; |
| | | } |
| | | .icon { |
| | | color: var(--action-0); |
| | | } |
| | | } |
| | | .footer .button { |
| | | width: 100%; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | //Nothing to see here |
| | | |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/menu", |
| | | "version": "0.1.0", |
| | | "title": "Our Menu", |
| | | "category": "jvb", |
| | | "icon": "food", |
| | | "description": "Outputs our menu, organized by categories", |
| | | "example": {}, |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "textdomain": "jvb", |
| | | "selectors": { |
| | | "root": ".menu-block" |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "render": "file:./render.php", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * Retrieves the translation of text. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/ |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | |
| | | /** |
| | | * React hook that is used to mark the block wrapper element. |
| | | * It provides all the necessary props like the class name. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops |
| | | */ |
| | | import { useBlockProps } from '@wordpress/block-editor'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * Those files can contain any CSS code that gets applied to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * The edit function describes the structure of your block in the context of the |
| | | * editor. This represents what the editor will render when the block is used. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit |
| | | * |
| | | * @return {Element} Element to render. |
| | | */ |
| | | export default function Edit() { |
| | | return ( |
| | | <p { ...useBlockProps() }> |
| | | { __( 'Will output the menu', 'jvb' ) } |
| | | </p> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | | ?> |
| | | <p <?php echo get_block_wrapper_attributes(); ?>> |
| | | <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?> |
| | | </p> |
| New file |
| | |
| | | window.details = document.querySelectorAll('details'); |
| | | window.toggles = document.querySelectorAll('.toggle-details'); |
| | | |
| | | document.addEventListener('click', (e) => { |
| | | if (e.target.classList.contains('toggle-details')) { |
| | | e.target.classList.toggle('open'); |
| | | let on = e.target.classList.contains('open'); |
| | | let section = e.target.dataset.toggle; |
| | | if (section === 'all') { |
| | | toggleToggles(on); |
| | | } |
| | | |
| | | let span = e.target.querySelector('span'); |
| | | span.textContent = (on) ? 'Close': 'Open'; |
| | | toggleDetails(section, on); |
| | | } |
| | | }); |
| | | |
| | | |
| | | console.log(window.details); |
| | | function toggleDetails(name, toggle) { |
| | | if (name === 'all') { |
| | | console.log('Toggling all!'); |
| | | window.details.forEach(detail => { |
| | | console.log(detail); |
| | | detail.open = toggle; |
| | | }); |
| | | } else { |
| | | for (let detail of window.details) { |
| | | if (detail.dataset.section === name) { |
| | | detail.open = toggle; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | function toggleToggles(on) { |
| | | window.toggles.forEach(toggle => { |
| | | if (toggle.dataset.toggle !== 'all') { |
| | | toggle.querySelector('span').textContent = (on) ? 'Close' : 'Open'; |
| | | } |
| | | }); |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/summary", |
| | | "title": "Archive Summary", |
| | | "category": "jvb", |
| | | "icon": "align-center", |
| | | "description": "Outputs the information for the given archive page, or the bio for a profile. Pairs well with the feed block.", |
| | | "keywords": [ "summary", "bio", "style", "term" ], |
| | | "version": "0.9.0", |
| | | "textdomain": "jvb", |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "selectors": { |
| | | "root": ".summary-block" |
| | | }, |
| | | "styles": [ |
| | | { "name": "default", "label": "Default", "isDefault": true } |
| | | ], |
| | | "example": { |
| | | "attributes": { |
| | | "listType": "tattoo" |
| | | } |
| | | }, |
| | | "render": "file:./render.php", |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * WordPress dependencies |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; |
| | | import { SelectControl, ToggleControl, PanelBody } from '@wordpress/components'; |
| | | |
| | | /** |
| | | * Styles |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * Edit function for Summary Block |
| | | */ |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const blockProps = useBlockProps(); |
| | | |
| | | return ( |
| | | <div {...blockProps}> |
| | | <div className="jvb-summary-preview"> |
| | | <h3>{__('Summary', 'jvb')}</h3> |
| | | <p className="jvb-list-preview-note"> |
| | | {__('This will inherit the current query to build the information from our custom meta on the front end.', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * Directory List Block Editor Styles |
| | | */ |
| | | .jvb-summary-preview { |
| | | padding: 20px; |
| | | background-color: #f8f9fa; |
| | | border: 1px solid #e2e4e7; |
| | | border-radius: 4px; |
| | | |
| | | h3 { |
| | | margin-top: 0; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #ff0080; |
| | | } |
| | | &-note { |
| | | font-style: italic; |
| | | color: #555d66; |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import save from './save'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | |
| | | /** |
| | | * @see ./save.js |
| | | */ |
| | | save, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Render; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | /** |
| | | * Summary Block Render |
| | | * |
| | | * @package Edmonton_Ink |
| | | */ |
| | | |
| | | function jvbRenderSummaryBlock(array $attributes):string |
| | | { |
| | | |
| | | // Buffer output |
| | | if (is_tax()) { |
| | | switch (get_queried_object()->taxonomy) { |
| | | case BASE.'shop': |
| | | return jvbRenderShopSummary(); |
| | | default: |
| | | return jvbRenderTermSummary(); |
| | | } |
| | | } elseif (is_singular()) { |
| | | return jvbRenderArtistSummary(); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function jvbRenderArtistSummary():string |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = Cache::for('artistSummary', WEEK_IN_SECONDS); |
| | | $key = $current->ID; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | $meta = Meta::forPost($current->ID); |
| | | $artist = jvbContentFromUser((int)$current->post_author); |
| | | |
| | | $registrar = Registrar::getInstance($current->post_type)); |
| | | $sections = []; |
| | | if ($registrar) { |
| | | $sections = $registrar->getSections(); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | // $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type)); |
| | | ?> |
| | | <nav id="artist" 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> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> |
| | | <li><a href="#about">About</a></li> |
| | | <li><a href="#styles">Styles</a></li> |
| | | <li><a href="#contact">Contact</a></li> |
| | | <li><a href="#work">Work</a></li> |
| | | </ul> |
| | | </nav> |
| | | <header id="top"> |
| | | <h1><small><?=(!empty($artist['city'])) ? $artist['city']['name'] :'Edmonton'?>'s Best <?= (!empty($artist['type'])) ? |
| | | $artist['type']['name']:'Tattoo Artists'?>: |
| | | </small><?=$artist['display_name']?></h1> |
| | | <div> |
| | | <?php if (!empty($artist['shop'])) : ?> |
| | | <ul class="term-list shop"> |
| | | <li> |
| | | <a href="<?=$artist['shop']['url']?>" title="Learn more about <?=$artist['shop']['name']?>"> |
| | | <?= strtolower($artist['shop']['name'])?> |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | <?php endif; ?> |
| | | <?php if (!empty($artist['city'])): ?> |
| | | <ul class="term-list city"> |
| | | <li> |
| | | <a href="<?=$artist['city']['url']?>" title="See who else is rocking out of <?=$artist['city']['name']?>"> |
| | | <?= strtolower($artist['city']['name'])?> |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | <?php endif; ?> |
| | | <?php $styles = $meta->get('top_styles'); |
| | | if (!empty($styles)) { |
| | | ?> |
| | | <ul class="term-list style"> |
| | | <?php |
| | | foreach ($styles as $style) { |
| | | $term = get_term((int)$style, BASE.'style'); |
| | | if ($term && !is_wp_error($term)) { |
| | | $link = get_term_link((int)$style, BASE.'style'); |
| | | ?> |
| | | <li> |
| | | <a href="<?=$link?>" title="Learn more about <?=html_entity_decode($term->name)?>"> |
| | | <?=strtolower(html_entity_decode($term->name))?> |
| | | </a> |
| | | </li> |
| | | <?php |
| | | } |
| | | } |
| | | ?> |
| | | </ul> |
| | | <?php |
| | | } |
| | | ?> |
| | | </div> |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row x-btw"> |
| | | <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2> |
| | | </summary> |
| | | <div class="columns stack-small"> |
| | | <div class="column"> |
| | | <?= Render::renderFrom($meta, 'image_portrait'); ?> |
| | | </div> |
| | | <div class="column"> |
| | | <?= Render::renderFrom($meta, 'short_bio'); ?> |
| | | </div> |
| | | </div> |
| | | <div id="styles"> |
| | | <h3>Works In</h3> |
| | | <?= jvbGetTheTerms('style', $current->ID) ?> |
| | | </div> |
| | | <div class="contact"> |
| | | <h3>Contact:</h3> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->ID, $meta); |
| | | echo jvbRenderLinks($current->ID, $meta); |
| | | ?> |
| | | </div> |
| | | |
| | | <div id="about"> |
| | | <?= Render::renderFrom($meta, 'bio')?> |
| | | </div> |
| | | </details> |
| | | </section> |
| | | <section id="contact" class=""> |
| | | <h2>Contact <?=$artist['name']?></h2> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->ID, 'post'); |
| | | echo jvbRenderLinks($current->ID, 'post'); |
| | | ?> |
| | | </section> |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| | | |
| | | function jvbRenderShopSummary() |
| | | { |
| | | $current = get_queried_object(); |
| | | |
| | | $cache = Cache::for('shop_bio', WEEK_IN_SECONDS)->connect('taxonomy'); |
| | | $key = $current->term_id; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | |
| | | $meta = Meta::forTerm($current->term_id); |
| | | $fields = $meta->getAll(['average_rating', 'established', 'bio','location','hours','specialties','awards','reviews']); |
| | | ?> |
| | | <nav id="shop" 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> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php |
| | | if ($fields['rating'] !== 'none') { |
| | | ?> |
| | | <li><a href="#rating">Rating</a></li> |
| | | <?php |
| | | } elseif ($fields['opened'] !== '') { |
| | | ?> |
| | | <li><a href="#opened">Opened</a></li> |
| | | <?php |
| | | } elseif ($fields['location'] !== '') { |
| | | ?> |
| | | <li><a href="#location">Location</a></li> |
| | | <?php |
| | | } elseif ($fields['about'] !== '') { |
| | | ?> |
| | | <li><a href="#about">About</a></li> |
| | | <?php |
| | | } elseif ($fields['hours'] !== '') { |
| | | ?> |
| | | <li><a href="#hours">Hours</a></li> |
| | | <?php |
| | | } elseif ($fields['specialties'] !== '') { |
| | | ?> |
| | | <li><a href="#specialties">Specialties</a></li> |
| | | <?php |
| | | } elseif ($fields['awards'] !== '') { |
| | | ?> |
| | | <li><a href="#awards">Awards</a></li> |
| | | <?php |
| | | } elseif ($fields['reviews'] !== '') { |
| | | ?> |
| | | <li><a href="#reviews">Reviews</a></li> |
| | | <?php |
| | | } |
| | | ?> |
| | | <li><a href="#contact">Contact</a></li> |
| | | <li><a href="#artists">Artists</a></li> |
| | | </ul> |
| | | </nav> |
| | | <header id="top"> |
| | | <div class="columns stack-small"> |
| | | <div class="column"> |
| | | <?=jvbFormatImage($meta->get('image'))?> |
| | | </div> |
| | | <div class="column"> |
| | | <h1> |
| | | <small><?= (get_term((int)$meta->get('city'), BASE.'city')) ? |
| | | get_term((int)$meta->get('city'), BASE.'city')->name : |
| | | 'Edmonton'?>'s Best Tattoo Shops</small> |
| | | <?=$current->name?> |
| | | </h1> |
| | | <?= jvbFormatRating($current->term_id, 'term') ?> |
| | | <?= Render::renderFrom($meta, 'slogan'); ?> |
| | | </div> |
| | | </div> |
| | | </header> |
| | | <section> |
| | | <details class="bio-info"> |
| | | <summary class="row x-btw"> |
| | | <h2>Learn More About <?=$current->name?></h2> |
| | | </summary> |
| | | <div class="map"> |
| | | <?= Render::renderFrom($meta, 'location'); ?> |
| | | </div> |
| | | <div class="short-bio"> |
| | | <?= Render::renderFrom($meta, 'short_bio'); ?> |
| | | </div> |
| | | |
| | | <div class="contact"> |
| | | <h3>Contact:</h3> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->term_id, 'term'); |
| | | echo jvbRenderLinks($current->term_id, 'term'); |
| | | ?> |
| | | </div> |
| | | |
| | | <div id="about"> |
| | | <?= Render::renderFrom($meta, 'bio')?> |
| | | </div> |
| | | </details> |
| | | </section> |
| | | <section id="contact" class=""> |
| | | <h2>Contact </h2> |
| | | <?php |
| | | echo jvbRenderContactInfo($current->term_id, 'term'); |
| | | echo jvbRenderLinks($current->term_id, 'term'); |
| | | ?> |
| | | </section> |
| | | <?= jvbRenderHours($current->term_id, 'term')?> |
| | | |
| | | |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| | | |
| | | |
| | | function jvbRenderTermSummary() |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = Cache::for('term_summary', WEEK_IN_SECONDS)->connect('taxonomy'); |
| | | $key = $current->ID; |
| | | $cached = $cache->get($key); |
| | | if ($cached !== false) { |
| | | return $cached; |
| | | } |
| | | |
| | | ob_start(); |
| | | $tax = jvbNoBase($current->taxonomy); |
| | | switch ($tax) { |
| | | case 'style': |
| | | $title = 'Tattoo Artists'; |
| | | break; |
| | | case 'theme': |
| | | $title = 'Tattoos'; |
| | | break; |
| | | default: |
| | | $title = ''; |
| | | } |
| | | |
| | | $meta = Meta::forTerm($current->ID); |
| | | $fields = $meta->getAll(); |
| | | |
| | | ?> |
| | | <header id="top"> |
| | | <h1><?= get_the_archive_title() ?></h1> |
| | | </header> |
| | | |
| | | <?php |
| | | $finished = ob_get_clean(); |
| | | $cache->set($key, $finished); |
| | | return $finished; |
| | | } |
| New file |
| | |
| | | export default function save() { |
| | | return null; // Dynamic block rendered by PHP |
| | | } |
| New file |
| | |
| | | details > div { |
| | | margin: 1rem 0; |
| | | } |
| | | |
| | | main > header:not(:has(img)) { |
| | | margin-top: 3rem!important; |
| | | } |
| | | |
| | | header a::before { |
| | | display: none!important; |
| | | } |
| | | |
| | | header + details { |
| | | margin: 1.5rem auto 3rem!important; |
| | | max-width: var(--wide); |
| | | } |
| | | |
| | | main { |
| | | padding-top: 0!important; |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/timeline", |
| | | "version": "0.1.0", |
| | | "title": "Timeline", |
| | | "category": "jvb", |
| | | "icon": "shortcode", |
| | | "description": "Outputs a single timeline post in a cool way", |
| | | "example": {}, |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"] |
| | | }, |
| | | "textdomain": "jvb", |
| | | "selectors": { |
| | | "root": ".timeline" |
| | | }, |
| | | "editorScript": "file:./index.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css", |
| | | "viewScript": "file:./view.js" |
| | | } |
| New file |
| | |
| | | /** |
| | | * Retrieves the translation of text. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/ |
| | | */ |
| | | import { __ } from '@wordpress/i18n'; |
| | | |
| | | /** |
| | | * React hook that is used to mark the block wrapper element. |
| | | * It provides all the necessary props like the class name. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops |
| | | */ |
| | | import { useBlockProps } from '@wordpress/block-editor'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * Those files can contain any CSS code that gets applied to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './editor.scss'; |
| | | |
| | | /** |
| | | * The edit function describes the structure of your block in the context of the |
| | | * editor. This represents what the editor will render when the block is used. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit |
| | | * |
| | | * @return {Element} Element to render. |
| | | */ |
| | | export default function Edit() { |
| | | return ( |
| | | <p { ...useBlockProps() }> |
| | | { __( 'Will output the timeline', 'jvb' ) } |
| | | </p> |
| | | ); |
| | | } |
| New file |
| | |
| | | /** |
| | | * Registers a new block provided a unique name and an object defining its behavior. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | |
| | | /** |
| | | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. |
| | | * All files containing `style` keyword are bundled together. The code used |
| | | * gets applied both to the front of your site and to the editor. |
| | | * |
| | | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css |
| | | */ |
| | | import './style.scss'; |
| | | |
| | | /** |
| | | * Internal dependencies |
| | | */ |
| | | import Edit from './edit'; |
| | | import metadata from './block.json'; |
| | | |
| | | /** |
| | | * Every block starts by registering a new block type definition. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ |
| | | */ |
| | | registerBlockType( metadata.name, { |
| | | /** |
| | | * @see ./edit.js |
| | | */ |
| | | edit: Edit, |
| | | } ); |
| New file |
| | |
| | | <?php |
| | | /** |
| | | * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render |
| | | */ |
| | | ?> |
| | | <p <?php echo get_block_wrapper_attributes(); ?>> |
| | | <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?> |
| | | </p> |
| New file |
| | |
| | | main { |
| | | --gap: 0; |
| | | section:last-of-type { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | #at-a-glance { |
| | | padding: 0 10vw; |
| | | --gap: 0; |
| | | img { |
| | | width: 100%; |
| | | height: auto; |
| | | border: 2px solid var(--action-0); |
| | | } |
| | | h3 { |
| | | font-size: var(--txt-x-small); |
| | | } |
| | | .before { |
| | | img { |
| | | border-right-width: 1px; |
| | | border-left: 0; |
| | | border-top: 0; |
| | | } |
| | | } |
| | | .after { |
| | | img { |
| | | border-left-width: 1px; |
| | | border-right: 0; |
| | | border-bottom: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .timeline-point.timeline-point { |
| | | --lineWidth: 1px; |
| | | --gap: 2rem; |
| | | padding: 0; |
| | | margin:0; |
| | | background-color: var(--base); |
| | | max-width: 100vw; |
| | | position: relative; |
| | | overflow: hidden; |
| | | img { |
| | | width: 40%; |
| | | border-radius: 4px; |
| | | position: sticky; |
| | | padding: .5rem; |
| | | } |
| | | .info { |
| | | padding: 1rem .5rem .5rem; |
| | | width: 60%; |
| | | position: relative; |
| | | h2 { |
| | | margin: 0 0 .5rem; |
| | | font-size: var(--txt-medium); |
| | | position: relative; |
| | | .icon { |
| | | --w: 2.5rem; |
| | | transform: rotate(-90deg); |
| | | position: absolute; |
| | | left: -2.5rem; |
| | | top: .25rem; |
| | | background-color: var(--action-100); |
| | | } |
| | | } |
| | | |
| | | } |
| | | &::before, |
| | | &::after { |
| | | content: ''; |
| | | display: block; |
| | | position: absolute; |
| | | left: 45%; |
| | | height: 100%; |
| | | width: var(--lineWidth); |
| | | background-color: var(--action-0); |
| | | //box-shadow: var(--action-shadow); |
| | | } |
| | | &::before { |
| | | height: 1rem; |
| | | } |
| | | &::after { |
| | | top: 4rem; |
| | | } |
| | | &#before-treatment::before, |
| | | &:last-of-type::after { |
| | | display: none; |
| | | } |
| | | } |
| | | @media (min-width:768px) { |
| | | #at-a-glance { |
| | | h3 { |
| | | font-size: var(--txt-x-large); |
| | | } |
| | | } |
| | | .timeline-point.timeline-point { |
| | | --gap: 4rem; |
| | | img { |
| | | width: 50%; |
| | | } |
| | | .info { |
| | | width: 50%; |
| | | padding: 25vh 1rem 1rem; |
| | | h2 { |
| | | .icon { |
| | | --w: 4rem; |
| | | left: -6.15rem; |
| | | top: 0; |
| | | } |
| | | } |
| | | a { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | } |
| | | |
| | | time { |
| | | text-transform: uppercase; |
| | | font-size: var(--txt-x-small); |
| | | } |
| | | } |
| | | &::before, |
| | | &::after { |
| | | left: calc(50% + 2rem); |
| | | } |
| | | |
| | | &::before { |
| | | height: calc(25vh - 2rem); |
| | | } |
| | | &::after { |
| | | top: calc(25vh + 6rem); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | "$schema": "https://schemas.wp.org/trunk/block.json", |
| | | "apiVersion": 3, |
| | | "name": "jvb/video", |
| | | "version": "1.0.0", |
| | | "title": "Video Cover", |
| | | "category": "jvb", |
| | | "icon": "video-alt3", |
| | | "description": "Self-hosted video cover with poster and multiple format support", |
| | | "supports": { |
| | | "html": false, |
| | | "align": ["wide", "full"], |
| | | "spacing": { |
| | | "margin": true, |
| | | "padding": true |
| | | }, |
| | | "color": { |
| | | "background": true, |
| | | "text": true |
| | | } |
| | | }, |
| | | "attributes": { |
| | | "title": { |
| | | "type": "string", |
| | | "default": "" |
| | | }, |
| | | "description": { |
| | | "type": "rich-text", |
| | | "default": "" |
| | | }, |
| | | "posterId": { |
| | | "type": "number", |
| | | "default": 0 |
| | | }, |
| | | "posterUrl": { |
| | | "type": "string", |
| | | "default": "" |
| | | }, |
| | | "videoSources": { |
| | | "type": "array", |
| | | "default": [], |
| | | "items": { |
| | | "type": "object", |
| | | "properties": { |
| | | "id": { |
| | | "type": "number" |
| | | }, |
| | | "url": { |
| | | "type": "string" |
| | | }, |
| | | "mime": { |
| | | "type": "string" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "fadeEffect": { |
| | | "type": "boolean", |
| | | "default": false |
| | | }, |
| | | "overlayOpacity": { |
| | | "type": "number", |
| | | "default": 0 |
| | | }, |
| | | "contentAlignment": { |
| | | "type": "string", |
| | | "default": "center" |
| | | }, |
| | | "minHeight": { |
| | | "type": "number", |
| | | "default": 0 |
| | | } |
| | | }, |
| | | "textdomain": "jvb", |
| | | "editorScript": "file:./index.js", |
| | | "viewScript": "file:./view.js", |
| | | "editorStyle": "file:./index.css", |
| | | "style": "file:./style-index.css" |
| | | } |
| New file |
| | |
| | | //edit.js |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { |
| | | useBlockProps, |
| | | InspectorControls, |
| | | MediaUpload, |
| | | MediaUploadCheck, |
| | | InnerBlocks, |
| | | useInnerBlocksProps |
| | | } from '@wordpress/block-editor'; |
| | | import { |
| | | PanelBody, |
| | | Button, |
| | | ToggleControl, |
| | | BaseControl, |
| | | RangeControl, |
| | | SelectControl |
| | | } from '@wordpress/components'; |
| | | import './editor.scss'; |
| | | |
| | | const ALLOWED_VIDEO_TYPES = ['video']; |
| | | |
| | | const INNER_BLOCKS_TEMPLATE = [ |
| | | ['core/heading', { |
| | | level: 1, |
| | | placeholder: 'Add heading...', |
| | | textAlign: 'center' |
| | | }], |
| | | ['core/paragraph', { |
| | | placeholder: 'Add description...', |
| | | align: 'center' |
| | | }], |
| | | ['core/buttons', { |
| | | layout: { type: 'flex', justifyContent: 'center' } |
| | | }] |
| | | ]; |
| | | |
| | | |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const { |
| | | posterId, |
| | | posterUrl, |
| | | videoSources, |
| | | fadeEffect, |
| | | overlayOpacity, |
| | | contentAlignment, |
| | | minHeight |
| | | } = attributes; |
| | | |
| | | const blockProps = useBlockProps({ |
| | | className: 'video-cover-editor', |
| | | style: { |
| | | minHeight: minHeight ? `${minHeight}px` : undefined |
| | | } |
| | | }); |
| | | |
| | | const innerBlocksProps = useInnerBlocksProps( |
| | | { className: 'video-cover-content' }, |
| | | { |
| | | template: INNER_BLOCKS_TEMPLATE, |
| | | templateLock: false |
| | | } |
| | | ); |
| | | |
| | | const onSelectPoster = (media) => { |
| | | setAttributes({ |
| | | posterId: media.id, |
| | | posterUrl: media.url |
| | | }); |
| | | }; |
| | | |
| | | const onSelectVideos = (mediaItems) => { |
| | | // multiple=true returns an array |
| | | const items = Array.isArray(mediaItems) ? mediaItems : [mediaItems]; |
| | | const newSources = items |
| | | .filter(media => !videoSources.some(s => s.id === media.id)) |
| | | .map(media => ({ |
| | | id: media.id, |
| | | url: media.url, |
| | | mime: media.mime |
| | | })); |
| | | |
| | | if (newSources.length) { |
| | | setAttributes({ |
| | | videoSources: [...videoSources, ...newSources] |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const removeVideoSource = (index) => { |
| | | const updated = [...videoSources]; |
| | | updated.splice(index, 1); |
| | | setAttributes({ videoSources: updated }); |
| | | }; |
| | | |
| | | const renderVideoSourceList = (sources, isMobile = false) => { |
| | | if (sources.length === 0) return null; |
| | | |
| | | return ( |
| | | <ul className="video-source-list"> |
| | | {sources.map((source, index) => ( |
| | | <li key={index} className="video-source-item"> |
| | | <span className="video-source-mime">{source.mime}</span> |
| | | <Button |
| | | isDestructive |
| | | isSmall |
| | | onClick={() => removeVideoSource(index, isMobile)} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | </li> |
| | | ))} |
| | | </ul> |
| | | ); |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('Video Settings', 'jvb')} initialOpen={true}> |
| | | <BaseControl |
| | | label={__('Poster Image', 'jvb')} |
| | | help={__('Image shown while video loads', 'jvb')} |
| | | > |
| | | <MediaUploadCheck> |
| | | <MediaUpload |
| | | onSelect={onSelectPoster} |
| | | allowedTypes={['image']} |
| | | value={posterId} |
| | | render={({ open }) => ( |
| | | <> |
| | | {posterUrl && ( |
| | | <img |
| | | src={posterUrl} |
| | | alt={__('Poster preview', 'jvb')} |
| | | style={{ maxWidth: '100%', marginBottom: '10px' }} |
| | | /> |
| | | )} |
| | | <Button |
| | | onClick={open} |
| | | variant={posterUrl ? 'secondary' : 'primary'} |
| | | > |
| | | {posterUrl |
| | | ? __('Change Poster', 'jvb') |
| | | : __('Select Poster', 'jvb')} |
| | | </Button> |
| | | {posterUrl && ( |
| | | <Button |
| | | isDestructive |
| | | onClick={() => setAttributes({ posterId: 0, posterUrl: '' })} |
| | | style={{ marginLeft: '10px' }} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | )} |
| | | </> |
| | | )} |
| | | /> |
| | | </MediaUploadCheck> |
| | | </BaseControl> |
| | | |
| | | <BaseControl |
| | | label={__('Video Sources', 'jvb')} |
| | | help={__('Add multiple formats for better browser support (mp4, webm, etc.)', 'jvb')} |
| | | > |
| | | {videoSources.length > 0 && ( |
| | | <ul className="video-source-list"> |
| | | {videoSources.map((source, index) => ( |
| | | <li key={index} className="video-source-item"> |
| | | <span className="video-source-mime">{source.mime}</span> |
| | | <Button |
| | | isDestructive |
| | | isSmall |
| | | onClick={() => removeVideoSource(index)} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | </li> |
| | | ))} |
| | | </ul> |
| | | )} |
| | | <MediaUploadCheck> |
| | | <MediaUpload |
| | | multiple={true} |
| | | onSelect={onSelectVideos} |
| | | allowedTypes={ALLOWED_VIDEO_TYPES} |
| | | render={({ open }) => ( |
| | | <Button onClick={open} variant="secondary"> |
| | | {__('Add Video', 'jvb')} |
| | | </Button> |
| | | )} |
| | | /> |
| | | </MediaUploadCheck> |
| | | </BaseControl> |
| | | |
| | | <ToggleControl |
| | | label={__('Fade Effect', 'jvb')} |
| | | help={__('Add fade class to video element', 'jvb')} |
| | | checked={fadeEffect} |
| | | onChange={(value) => setAttributes({ fadeEffect: value })} |
| | | /> |
| | | </PanelBody> |
| | | |
| | | <PanelBody title={__('Overlay Settings', 'jvb')} initialOpen={true}> |
| | | <RangeControl |
| | | label={__('Overlay Opacity', 'jvb')} |
| | | help={__('Darken video for better text readability', 'jvb')} |
| | | value={overlayOpacity} |
| | | onChange={(value) => setAttributes({ overlayOpacity: value })} |
| | | min={0} |
| | | max={100} |
| | | step={5} |
| | | /> |
| | | |
| | | <SelectControl |
| | | label={__('Content Alignment', 'jvb')} |
| | | value={contentAlignment} |
| | | options={[ |
| | | { label: __('Top Left', 'jvb'), value: 'top-left' }, |
| | | { label: __('Top Center', 'jvb'), value: 'top-center' }, |
| | | { label: __('Top Right', 'jvb'), value: 'top-right' }, |
| | | { label: __('Center Left', 'jvb'), value: 'center-left' }, |
| | | { label: __('Center', 'jvb'), value: 'center' }, |
| | | { label: __('Center Right', 'jvb'), value: 'center-right' }, |
| | | { label: __('Bottom Left', 'jvb'), value: 'bottom-left' }, |
| | | { label: __('Bottom Center', 'jvb'), value: 'bottom-center' }, |
| | | { label: __('Bottom Right', 'jvb'), value: 'bottom-right' } |
| | | ]} |
| | | onChange={(value) => setAttributes({ contentAlignment: value })} |
| | | /> |
| | | |
| | | <RangeControl |
| | | label={__('Minimum Height', 'jvb')} |
| | | help={__('Minimum height in pixels (leave 0 for auto)', 'jvb')} |
| | | value={minHeight} |
| | | onChange={(value) => setAttributes({ minHeight: value })} |
| | | min={0} |
| | | max={1000} |
| | | step={50} |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div {...blockProps}> |
| | | {posterUrl || videoSources.length > 0 ? ( |
| | | <div className="video-cover-preview"> |
| | | {posterUrl && ( |
| | | <> |
| | | <img src={posterUrl} alt={__('Video poster', 'jvb')} /> |
| | | {overlayOpacity > 0 && ( |
| | | <div |
| | | className="video-overlay-preview" |
| | | style={{ opacity: overlayOpacity / 100 }} |
| | | /> |
| | | )} |
| | | </> |
| | | )} |
| | | <div className={`video-cover-content-preview align-${contentAlignment}`}> |
| | | <div {...innerBlocksProps} /> |
| | | </div> |
| | | <div className="video-info"> |
| | | <p> |
| | | {videoSources.length} {__('desktop source(s)', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ) : ( |
| | | <div className="video-cover-placeholder"> |
| | | <p>{__('Configure video sources in the sidebar →', 'jvb')}</p> |
| | | </div> |
| | | )} |
| | | </div> |
| | | </> |
| | | ); |
| | | } |
| New file |
| | |
| | | /* editor.scss */ |
| | | .video-cover-editor { |
| | | position: relative; |
| | | min-height: 200px; |
| | | background: #f0f0f0; |
| | | border: 2px dashed #ccc; |
| | | border-radius: 4px; |
| | | |
| | | .video-cover-preview { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 300px; |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: auto; |
| | | display: block; |
| | | } |
| | | |
| | | .video-overlay-preview { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 1); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .video-cover-content-preview { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | display: flex; |
| | | z-index: 2; |
| | | padding: 2rem; |
| | | |
| | | // Content alignment classes |
| | | &.align-top-left { |
| | | align-items: flex-start; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-top-center { |
| | | align-items: flex-start; |
| | | justify-content: center; |
| | | } |
| | | &.align-top-right { |
| | | align-items: flex-start; |
| | | justify-content: flex-end; |
| | | } |
| | | &.align-center-left { |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-center { |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | &.align-center-right { |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | } |
| | | &.align-bottom-left { |
| | | align-items: flex-end; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-bottom-center { |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | } |
| | | &.align-bottom-right { |
| | | align-items: flex-end; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .video-cover-content { |
| | | width: 100%; |
| | | max-width: 1200px; |
| | | color: white; |
| | | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); |
| | | |
| | | // Style inner blocks for better visibility in editor |
| | | h1, h2, h3, h4, h5, h6 { |
| | | color: white; |
| | | } |
| | | |
| | | p { |
| | | color: white; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .video-info { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: rgba(0, 0, 0, 0.7); |
| | | color: white; |
| | | padding: 10px; |
| | | font-size: 14px; |
| | | z-index: 3; |
| | | |
| | | p { |
| | | margin: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .video-cover-placeholder { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-height: 200px; |
| | | color: #666; |
| | | font-size: 16px; |
| | | } |
| | | } |
| | | |
| | | .video-source-list { |
| | | list-style: none; |
| | | margin: 10px 0; |
| | | padding: 0; |
| | | |
| | | .video-source-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 8px 12px; |
| | | background: #f5f5f5; |
| | | border-radius: 4px; |
| | | margin-bottom: 5px; |
| | | |
| | | .video-source-mime { |
| | | font-family: monospace; |
| | | font-size: 13px; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* index.js */ |
| | | import { registerBlockType } from '@wordpress/blocks'; |
| | | import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; |
| | | import './style.scss'; |
| | | import Edit from './edit'; |
| | | import metadata from './block.json'; |
| | | |
| | | registerBlockType(metadata.name, { |
| | | edit: Edit, |
| | | save: ({ attributes }) => { |
| | | const blockProps = useBlockProps.save({ |
| | | className: 'video-cover-wrapper-placeholder' |
| | | }); |
| | | |
| | | return ( |
| | | <div {...blockProps}> |
| | | <InnerBlocks.Content /> |
| | | </div> |
| | | ); |
| | | } |
| | | }); |
| New file |
| | |
| | | /** |
| | | * save.js |
| | | * React hook that is used to mark the block wrapper element. |
| | | * It provides all the necessary props like the class name. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops |
| | | */ |
| | | import { useBlockProps } from '@wordpress/block-editor'; |
| | | |
| | | /** |
| | | * The save function defines the way in which the different attributes should |
| | | * be combined into the final markup, which is then serialized by the block |
| | | * editor into `post_content`. |
| | | * |
| | | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save |
| | | * |
| | | * @return {WPElement} Element to render. |
| | | */ |
| | | export default function save() { |
| | | // This is a dynamic block that is rendered on the server side |
| | | // Return null to let WordPress handle the saving and rendering |
| | | return null; |
| | | } |
| New file |
| | |
| | | /** style.scss **/ |
| | | .video-cover { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 75vh; |
| | | overflow: hidden; |
| | | display: flex; |
| | | .wrap { |
| | | background-color: var(--contrast-200); |
| | | //&::before { |
| | | // position: absolute; |
| | | // top: 0; |
| | | // bottom: 0; |
| | | // left: 0; |
| | | // right: 0; |
| | | // background-color: var(--base); |
| | | // mix-blend-mode: lighten; |
| | | // content: ''; |
| | | // z-index: 1; |
| | | //} |
| | | } |
| | | /* Video background */ |
| | | .video-container { |
| | | position: absolute; |
| | | top: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | min-width: 100%; |
| | | min-height: 100%; |
| | | z-index: 0; |
| | | display: flex; |
| | | background-color: var(--action-50); |
| | | |
| | | &.fade { |
| | | animation: fadeIn 1s ease-in; |
| | | } |
| | | |
| | | video { |
| | | pointer-events: none; |
| | | opacity: .85; |
| | | mix-blend-mode: multiply; |
| | | filter: grayscale(100%) contrast(1); |
| | | flex: 1 0 100%; |
| | | object-fit: cover; |
| | | } |
| | | } |
| | | |
| | | .inner-wrap { |
| | | position: relative; |
| | | z-index: 2; |
| | | width: 100%; |
| | | padding: 2rem; |
| | | color: var(--action-contrast); |
| | | |
| | | /* Better text readability */ |
| | | h1, h2, h3, h4, h5, h6 { |
| | | word-spacing: 100vw; |
| | | color: var(--action-contrast); |
| | | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); |
| | | margin: 2rem 0 0; |
| | | } |
| | | |
| | | p { |
| | | text-transform: uppercase; |
| | | letter-spacing: 2px; |
| | | margin: 0; |
| | | color: var(--action-contrast); |
| | | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .media-text { |
| | | } |
| | | .media-text figure { |
| | | max-width: 50%; |
| | | } |
| | | @media (min-width: 768px) { |
| | | .media-text { |
| | | --align: flex-start; |
| | | gap: 3rem; |
| | | max-width: var(--content); |
| | | } |
| | | } |
| | | .media-text > div { |
| | | width: fit-content; |
| | | } |
| | | .buttons a { |
| | | font-weight: var(--fw-h-bold); |
| | | color: var(--action-contrast); |
| | | border-color: var(--action-contrast); |
| | | &:visited { |
| | | color: var(--action-contrast); |
| | | &:hover { |
| | | color: var(--action-contrast); |
| | | } |
| | | } |
| | | &:hover { |
| | | background-color: var(--action-0); |
| | | color: var(--action-contrast); |
| | | } |
| | | } |
| | | |
| | | .outline a { |
| | | background-color: rgba(var(--base-rgb), rgba(var(--base-rgb),var(--op-3))); |
| | | } |
| | | .buttons { |
| | | margin: 3rem 0; |
| | | li { |
| | | background-color: rgba(var(--action-rgb), var(--op-4)); |
| | | } |
| | | } |
| | | /* Button styles */ |
| | | .wp-block-button__link { |
| | | text-shadow: none; |
| | | } |
| | | } |
| | | |
| | | /* Alignment classes */ |
| | | &.align-top-left { |
| | | align-items: flex-start; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-top-center { |
| | | align-items: flex-start; |
| | | justify-content: center; |
| | | } |
| | | &.align-top-right { |
| | | align-items: flex-start; |
| | | justify-content: flex-end; |
| | | } |
| | | &.align-center-left { |
| | | align-items: center; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-center { |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | &.align-center-right { |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | } |
| | | &.align-bottom-left { |
| | | align-items: flex-end; |
| | | justify-content: flex-start; |
| | | } |
| | | &.align-bottom-center { |
| | | align-items: flex-end; |
| | | justify-content: center; |
| | | } |
| | | &.align-bottom-right { |
| | | align-items: flex-end; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | /* Full-width alignment */ |
| | | &.alignfull { |
| | | width: 100vw; |
| | | max-width: none; |
| | | margin-left: calc(50% - 50vw); |
| | | margin-right: calc(50% - 50vw); |
| | | } |
| | | |
| | | /* Wide alignment */ |
| | | &.alignwide { |
| | | max-width: 1200px; |
| | | } |
| | | } |
| | | |
| | | @keyframes fadeIn { |
| | | from { |
| | | opacity: 0; |
| | | } |
| | | to { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | /** view.js **/ |
| | | document.addEventListener("DOMContentLoaded", function () { |
| | | const lazyVideos = [].slice.call( |
| | | document.querySelectorAll(".video-container video") |
| | | ); |
| | | |
| | | // Build a helper to actually set sources + load |
| | | function loadVideo(video) { |
| | | const sources = video.querySelectorAll("source[data-src]"); |
| | | sources.forEach(source => { |
| | | source.src = source.dataset.src; |
| | | }); |
| | | video.load(); |
| | | } |
| | | |
| | | // --- 1. IntersectionObserver (best case) --- |
| | | if ("IntersectionObserver" in window) { |
| | | const lazyVideoObserver = new IntersectionObserver( |
| | | function (entries, observer) { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | loadVideo(entry.target); |
| | | observer.unobserve(entry.target); |
| | | } |
| | | }); |
| | | }, |
| | | { |
| | | rootMargin: "200px 0px", |
| | | threshold: 0.1, |
| | | } |
| | | ); |
| | | |
| | | lazyVideos.forEach(video => lazyVideoObserver.observe(video)); |
| | | return; |
| | | } |
| | | |
| | | // --- 2. Fallback: requestIdleCallback --- |
| | | if ("requestIdleCallback" in window) { |
| | | requestIdleCallback(() => { |
| | | lazyVideos.forEach(video => loadVideo(video)); |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // --- 3. Final fallback: load immediately --- |
| | | lazyVideos.forEach(video => loadVideo(video)); |
| | | }); |
| | |
| | | </div> |
| | | |
| | | <details class="type-filters"> |
| | | <summary class="row btw">Filters: |
| | | <summary class="row x-btw">Filters: |
| | | <button class="clear-filters row"> |
| | | <?= jvbIcon('x', ['title' => 'Clear Filters'])?> |
| | | <span>Clear Filters</span> |
| | |
| | | </dialog> |
| | | <template class="template-own"> |
| | | <details class="news item" data-keyboard-nav="true" tabindex="0"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <div class="item-select"> |
| | | <input type="checkbox" class="select-checkbox"> |
| | | <label> |
| | |
| | | </template> |
| | | <template class="template-all template-watching"> |
| | | <details class="news item" data-keyboard-nav="true" tabindex="0"> |
| | | <summary class="row btw"> |
| | | <summary class="row x-btw"> |
| | | <button class="favourite-button" data-type="news" title="Add to watch list" onclick="toggleFavourite(this)"> |
| | | |
| | | </button> |
| | |
| | | |
| | | ?> |
| | | <div class="container"> |
| | | <nav class="tabs row start" role="tablist"> |
| | | <nav class="tabs row left" role="tablist"> |
| | | <?php |
| | | |
| | | $i =0; |
| | |
| | | 'quill': './assets/js/concise/quill.js', |
| | | 'queue': './assets/js/concise/Queue.js', |
| | | 'referral': './assets/js/concise/Referral.js', |
| | | 'referralAdmin': './assets/js/concise/ReferralAdmin.js', |
| | | 'referralAdmin': './assets/js/concise/ReferralAdmin.js', |
| | | 'shopManager': './assets/js/concise/ShopManager.js', |
| | | 'cache': './assets/js/concise/SimpleCache.js', |
| | | 'schema': './assets/js/concise/SchemaManager.js', |
| | | 'square': './assets/js/concise/CheckoutSquare.js', |
| | | 'helcim': './assets/js/concise/CheckoutHelcim.js', |
| | | 'checkout': './assets/js/concise/Checkout.js', |
| | | 'checkout': './assets/js/concise/Checkout.js', |
| | | 'tabs': './assets/js/concise/Tabs.js', |
| | | 'creator': './assets/js/concise/TaxonomyCreator.js', |
| | | 'selector': './assets/js/concise/TaxonomySelector.js', |