/** * wpShopGermany, Modul Produktansichten: Seiten, Sortierung, Ansicht und Filter ohne Neuladen * * Ohne JavaScript funktionieren Links und Formulare über die Adresse. Mit JavaScript holt das Skript die Ansicht per * Ajax (wpsg_mod=wpsg_mod_productview, wpsg_action=refresh) und schreibt den Zustand in die Adresse. Die Attribute des * Shortcodes kommen signiert aus dem Feld wpsg_atts; der Server übernimmt nur Seite, Sortierung, Ansicht und Filter. * Produktlisten mit data-wpsg-filter (Block Produktliste, Produktkacheln) holt es mit der neuen Adresse als ganze Seite, * wie ohne JavaScript, und tauscht ihren Inhalt; die Statuszeile und ein Filter darin bleiben stehen, damit Ansage und * Fokus erhalten bleiben. */ ( function() { 'use strict'; function flatten( prefix, value, params ) { if ( value !== null && typeof value === 'object' ) { Object.keys( value ).forEach( function( k ) { flatten( prefix + '[' + k + ']', value[ k ], params ); } ); } else if ( value !== undefined && value !== null ) { params.append( prefix, String( value ) ); } return params; } function filterValues() { const form = document.getElementById( 'wpsg_mod_productview_filter' ); const values = {}; if ( !form ) return values; new FormData( form ).forEach( function( v, name ) { const m = name.match( /^pvf\[(\w+)\](?:\[(\d+)\])?/ ); if ( !m || v === '' ) return; if ( m[ 2 ] ) { values[ m[ 1 ] ] = values[ m[ 1 ] ] || {}; values[ m[ 1 ] ][ m[ 2 ] ] = values[ m[ 1 ] ][ m[ 2 ] ] ? values[ m[ 1 ] ][ m[ 2 ] ] + ',' + v : v; } else values[ m[ 1 ] ] = v; } ); return values; } // Laufende Anfragen je Ansicht (Position auf der Seite) und für die Produktlisten: Nur die zuletzt gestellte zählt const pending = []; let pendingLists = null; function refresh( box, change, url, push ) { const field = box.querySelector( 'input[name="wpsg_atts"]' ); if ( !field ) return false; const index = Array.prototype.indexOf.call( document.querySelectorAll( '.wpsg_productview' ), box ); if ( pending[ index ] ) pending[ index ].abort(); const ctl = pending[ index ] = new AbortController(); const atts = JSON.parse( field.value ); Object.assign( atts, change ); const params = flatten( 'wpsg_atts', atts, new URLSearchParams( { wpsg_mod: 'wpsg_mod_productview', wpsg_action: 'refresh' } ) ); box.classList.add( 'is-loading' ); box.setAttribute( 'aria-busy', 'true' ); fetch( box.getAttribute( 'data-wpsg-pv-url' ) + ( box.getAttribute( 'data-wpsg-pv-url' ).indexOf( '?' ) === -1 ? '?' : '&' ) + params.toString(), { credentials: 'same-origin', signal: ctl.signal } ) .then( function( r ) { if ( !r.ok ) throw new Error( r.status ); return r.text(); } ) .then( function( html ) { if ( pending[ index ] !== ctl ) return; pending[ index ] = null; const tmp = document.createElement( 'div' ); tmp.innerHTML = html; const fresh = tmp.querySelector( '.wpsg_productview' ); if ( !fresh ) throw new Error( 'leer' ); fresh.querySelectorAll( '.wpsg-pv__order-submit' ).forEach( function( b ) { b.hidden = true; } ); // Eine frühere Antwort kann die Ansicht schon ersetzt haben document.querySelectorAll( '.wpsg_productview' )[ index ].replaceWith( fresh ); if ( url && push !== false ) history.pushState( { wpsgPv: true }, '', url ); } ) .catch( function() { if ( url && pending[ index ] === ctl ) location.href = url; } ); return true; } document.addEventListener( 'click', function( e ) { const a = e.target.closest( '.wpsg-pv__pager a, .wpsg-pv__modes a' ); if ( !a ) return; const box = a.closest( '.wpsg_productview' ); const change = a.hasAttribute( 'data-page' ) ? { page: a.getAttribute( 'data-page' ) } : { mode: a.getAttribute( 'data-mode' ) }; if ( refresh( box, change, a.href ) ) e.preventDefault(); } ); document.addEventListener( 'change', function( e ) { const select = e.target.closest( '.wpsg-pv__order select' ); if ( select ) { const form = select.form; const url = form.action + ( form.action.indexOf( '?' ) === -1 ? '?' : '&' ) + new URLSearchParams( new FormData( form ) ).toString(); refresh( select.closest( '.wpsg_productview' ), { order: select.value, page: 1 }, url ); return; } if ( e.target.closest( '#wpsg_mod_productview_filter' ) ) applyFilter(); } ); document.addEventListener( 'submit', function( e ) { if ( e.target.id !== 'wpsg_mod_productview_filter' ) return; if ( applyFilter() ) e.preventDefault(); } ); function refreshLists( lists, url ) { lists.forEach( function( list ) { list.setAttribute( 'aria-busy', 'true' ); } ); if ( pendingLists ) pendingLists.abort(); const ctl = pendingLists = new AbortController(); fetch( url, { credentials: 'same-origin', signal: ctl.signal } ) .then( function( r ) { if ( !r.ok ) throw new Error( r.status ); return r.text(); } ) .then( function( html ) { if ( pendingLists !== ctl ) return; pendingLists = null; const fresh = new DOMParser().parseFromString( html, 'text/html' ).querySelectorAll( '[data-wpsg-filter]' ); if ( fresh.length !== lists.length ) throw new Error( 'anders' ); lists.forEach( function( list, i ) { const status = list.querySelector( ':scope > .wpsg-product-list__count' ); const keep = function( child ) { return child.classList.contains( 'wpsg-product-list__count' ) || child.classList.contains( 'wpsg-block-product-filter' ); }; Array.prototype.slice.call( list.children ).forEach( function( child ) { if ( !keep( child ) ) child.remove(); } ); Array.prototype.slice.call( fresh[ i ].children ).forEach( function( child ) { if ( !keep( child ) ) list.appendChild( document.importNode( child, true ) ); } ); const freshStatus = fresh[ i ].querySelector( ':scope > .wpsg-product-list__count' ); if ( status && freshStatus ) status.textContent = freshStatus.textContent; list.removeAttribute( 'aria-busy' ); } ); } ) .catch( function() { if ( pendingLists === ctl ) location.href = url; } ); } // Zahl der aktiven Filter im Knopf „Filter“ und „Filter zurücksetzen“ der Leiste; der Filter bleibt beim Tauschen stehen function updateBar( form ) { const badge = form.querySelector( '.wpsg-pvf__badge' ); const reset = form.querySelector( '.wpsg-pvf__toggle .wpsg-pvf__reset' ); if ( !badge && !reset ) return; const groups = {}; new FormData( form ).forEach( function( v, name ) { if ( v === '' ) return; const m = name.match( /^pvf\[(variants|attributs)\]\[(\d+)\]/ ); if ( name === 'pvc' ) groups.cat = true; else if ( /^pvf\[price_(min|max)\]$/.test( name ) ) groups.price = true; else if ( m ) groups[ m[ 1 ] + m[ 2 ] ] = true; } ); const n = Object.keys( groups ).length; if ( badge ) { badge.hidden = ( n === 0 ); badge.querySelector( '.wpsg-pvf__badge-count' ).textContent = String( n ); } if ( reset ) reset.hidden = ( n === 0 ); } function applyFilter() { const form = document.getElementById( 'wpsg_mod_productview_filter' ); const boxes = document.querySelectorAll( '.wpsg_productview' ); const lists = document.querySelectorAll( '[data-wpsg-filter]' ); if ( !form || ( !boxes.length && !lists.length ) ) return false; const params = new URLSearchParams(); new FormData( form ).forEach( function( v, name ) { if ( v !== '' && !/^pv-/.test( name ) ) params.append( name, v ); } ); // Sortierung und Ansicht aus der aktuellen Adresse behalten, die Seite beginnt neu new URLSearchParams( location.search ).forEach( function( v, name ) { if ( /^pv-.*(order|mode)$/.test( name ) ) params.append( name, v ); } ); const url = form.action + ( form.action.indexOf( '?' ) === -1 ? '?' : '&' ) + params.toString(); const values = filterValues(); boxes.forEach( function( box ) { refresh( box, { filter: values, page: 1 }, url, false ); } ); if ( lists.length ) refreshLists( lists, url ); updateBar( form ); history.pushState( { wpsgPv: true }, '', url ); return true; } // Zurück und Vor: den Stand aus der Adresse neu laden window.addEventListener( 'popstate', function( e ) { if ( e.state && e.state.wpsgPv ) location.reload(); } ); // Mit JavaScript sortiert die Auswahl sofort document.querySelectorAll( '.wpsg-pv__order-submit' ).forEach( function( b ) { b.hidden = true; } ); // Filterleiste der Produktkacheln: am Handy zugeklappt, breit immer offen (Grenze wie WordPress, 782 px) const narrow = window.matchMedia( '(max-width: 781px)' ); function toggleBar() { document.querySelectorAll( '.wpsg-pvf__toggle' ).forEach( function( d ) { d.open = !narrow.matches; } ); } toggleBar(); narrow.addEventListener( 'change', toggleBar ); } )();