summaryrefslogtreecommitdiff
path: root/views
diff options
context:
space:
mode:
Diffstat (limited to 'views')
-rw-r--r--views/css/frontend.css47
-rw-r--r--views/css/wpsg-theme.css50
-rw-r--r--views/mods/mod_productview/productview.js148
-rw-r--r--views/mods/mod_productview/productview.phtml171
-rw-r--r--views/mods/mod_productview/productview_filter.phtml83
-rw-r--r--views/mods/mod_productview/settings_edit.phtml10
6 files changed, 507 insertions, 2 deletions
diff --git a/views/css/frontend.css b/views/css/frontend.css
index c2354f3..eff9f08 100644
--- a/views/css/frontend.css
+++ b/views/css/frontend.css
@@ -724,3 +724,50 @@ html body .wpsg_product_garan_wrap a.wpsg_product_garan img { display:block; max
@media (max-width:600px) {
.wpsg-block-product-list .wpsg-product-list__items { grid-template-columns:repeat(min(var(--wpsg-columns, 3), 2), minmax(0, 1fr)); }
}
+/* Produktansichten (Modul, Vorlage seit 4.6.1, nur neue Klassen) */
+.wpsg-pv .wpsg-pv__head { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em 1rem; margin-bottom:1rem; }
+.wpsg-pv .wpsg-pv__title { flex:1 1 100%; margin:0; }
+.wpsg-pv .wpsg-pv__info { flex:1 1 auto; margin:0; font-size:0.8em; opacity:0.8; }
+.wpsg-pv .wpsg-pv__order { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; margin:0; }
+.wpsg-pv .wpsg-pv__order label { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; }
+.wpsg-pv .wpsg-pv__modes { display:flex; gap:0.75em; }
+.wpsg-pv .wpsg-pv__modes a.active { font-weight:bold; text-decoration:none; }
+.wpsg-pv .wpsg-pv__items { display:grid; grid-template-columns:repeat(var(--wpsg-columns, 4), minmax(0, 1fr)); gap:calc(1rem * 2) 1rem; list-style:none; margin:0; padding:0; }
+.wpsg-pv .wpsg-pv__item { display:flex; flex-direction:column; gap:0.5em; min-width:0; margin:0; padding:0; }
+.wpsg-pv .wpsg-pv__image img { display:block; width:100%; height:auto; }
+.wpsg-pv .wpsg-pv__body { display:flex; flex-direction:column; gap:0.35em; flex:1 1 auto; min-width:0; }
+.wpsg-pv .wpsg-pv__body > p, .wpsg-pv .wpsg-pv__description { margin:0; }
+.wpsg-pv .wpsg-pv__name { margin:0; font-size:1em; line-height:1.3; overflow-wrap:anywhere; hyphens:auto; }
+.wpsg-pv .wpsg-pv__price { font-size:1.2em; }
+.wpsg-pv .wpsg-pv__price-old { margin-left:0.5em; font-size:0.8em; opacity:0.8; }
+.wpsg-pv .wpsg-pv__detailname, .wpsg-pv .wpsg-pv__taxnote, .wpsg-pv .wpsg-pv__baseprice, .wpsg-pv .wpsg-pv__stock, .wpsg-pv .wpsg-pv__deliverytime, .wpsg-pv .wpsg-pv__weight { font-size:0.8em; opacity:0.8; }
+.wpsg-pv .wpsg-pv__description { display:none; }
+.wpsg-pv .wpsg-pv__action { display:flex; flex-direction:column; gap:0.5em; margin-top:auto; padding-top:0.5em; }
+.wpsg-pv .wpsg-pv__action form { margin:0; }
+.wpsg-pv .wpsg-pv__action .wpsg_button { display:block; width:100%; margin:0; text-align:center; box-sizing:border-box; }
+.wpsg-pv--list .wpsg-pv__items { grid-template-columns:minmax(0, 1fr); }
+.wpsg-pv--list .wpsg-pv__item { flex-direction:row; gap:1rem; padding-bottom:1rem; border-bottom:1px solid #EAEAEA; }
+.wpsg-pv--list .wpsg-pv__image { flex:0 0 min(30%, 220px); }
+.wpsg-pv--list .wpsg-pv__description { display:block; }
+.wpsg-pv--list .wpsg-pv__action .wpsg_button { display:inline-block; width:auto; }
+.wpsg-pv--list .wpsg-pv__action { flex-direction:row; flex-wrap:wrap; }
+.wpsg-pv .wpsg-pv__pager { display:flex; flex-wrap:wrap; justify-content:center; gap:0.5em; margin-top:calc(1rem * 2); }
+.wpsg-pv .wpsg-pv__pager a, .wpsg-pv .wpsg-pv__pager span { min-width:2em; padding:0.25em 0.5em; text-align:center; border:1px solid #CCCCCC; border-radius:3px; }
+.wpsg-pv .wpsg-pv__pager span[aria-current] { font-weight:bold; background-color:#F5F5F5; }
+.wpsg-pv .wpsg-pv__pager .wpsg-pv__gap { border-color:transparent; }
+.wpsg-pv.is-loading { opacity:0.5; pointer-events:none; }
+.wpsg-pvf .wpsg-pvf__item { margin:0 0 1rem 0; padding:0; border:0; min-width:0; }
+.wpsg-pvf .wpsg-pvf__title { margin:0 0 0.5em 0; padding:0; font-size:1em; font-weight:bold; }
+.wpsg-pvf .wpsg-pvf__item ul { margin:0; padding-left:1.2em; }
+.wpsg-pvf .wpsg-pvf__price { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; }
+.wpsg-pvf .wpsg-pvf__price label { display:flex; align-items:center; gap:0.35em; }
+.wpsg-pvf .wpsg-pvf__price input { width:7em; }
+.wpsg-pvf .wpsg-pvf__option { display:flex; align-items:center; gap:0.5em; margin:0.25em 0; }
+.wpsg-pvf .wpsg-pvf__color { display:inline-block; width:1.2em; height:1.2em; border:1px solid #CCCCCC; border-radius:3px; }
+.wpsg-pvf .wpsg-pvf__actions { display:flex; flex-wrap:wrap; align-items:center; gap:1rem; }
+
+@media (max-width:600px) {
+ .wpsg-pv .wpsg-pv__items { grid-template-columns:repeat(min(var(--wpsg-columns, 4), 2), minmax(0, 1fr)); }
+ .wpsg-pv--list .wpsg-pv__items { grid-template-columns:minmax(0, 1fr); }
+ .wpsg-pv--list .wpsg-pv__item { flex-direction:column; }
+}
diff --git a/views/css/wpsg-theme.css b/views/css/wpsg-theme.css
index a0403e7..eef6abf 100644
--- a/views/css/wpsg-theme.css
+++ b/views/css/wpsg-theme.css
@@ -72,7 +72,7 @@
* Eingabefelder: schlichter Rückfall für Themes, die Felder nicht gestalten.
* Gestaltet das Theme input/select/textarea, gewinnt es.
*/
-:where(.wpsg, .wpsg_productform, .wpsg_mod_productindex, .wpsg_basket_widget, .wpsg_mod_kundenverwaltung_widget, #wpsg_calc_layer) :where(input[type="text"], input[type="email"], input[type="tel"], input[type="number"], input[type="password"], input[type="date"], input[type="search"], input:not([type]), select, textarea) {
+:where(.wpsg, .wpsg_productform, .wpsg_mod_productindex, .wpsg_basket_widget, .wpsg_mod_kundenverwaltung_widget, #wpsg_calc_layer, .wpsg-pv, .wpsg-pvf) :where(input[type="text"], input[type="email"], input[type="tel"], input[type="number"], input[type="password"], input[type="date"], input[type="search"], input:not([type]), select, textarea) {
font:inherit;
color:inherit;
padding:var(--wpsg-field-padding);
@@ -81,7 +81,7 @@
background-color:var(--wpsg-field-background);
max-width:100%;
}
-:where(.wpsg, .wpsg_productform, .wpsg_mod_productindex, .wpsg_basket_widget, .wpsg_mod_kundenverwaltung_widget, #wpsg_calc_layer) :where(select) { background-color:var(--wpsg-color-background); color:var(--wpsg-color-foreground); height:auto; }
+:where(.wpsg, .wpsg_productform, .wpsg_mod_productindex, .wpsg_basket_widget, .wpsg_mod_kundenverwaltung_widget, #wpsg_calc_layer, .wpsg-pv, .wpsg-pvf) :where(select) { background-color:var(--wpsg-color-background); color:var(--wpsg-color-foreground); height:auto; }
:where(.wpsg, .wpsg_productform) :where(textarea) { resize:vertical; }
/*
@@ -587,6 +587,48 @@ html body .wpsg_product_garan_wrap a.wpsg_product_garan img { display:block; max
.wpsg-block-mini-cart :where(.wpsg-mini-cart__content) { display:inline-flex; gap:0.5em; }
.wpsg-block-mini-cart :where(.wpsg-mini-cart__sum) { font-weight:bold; }
+/* Produktansichten (Modul, Vorlage seit 4.6.1) */
+.wpsg-pv :where(.wpsg-pv__head) { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em var(--wpsg-gap); margin-bottom:var(--wpsg-gap); }
+.wpsg-pv :where(.wpsg-pv__title) { flex:1 1 100%; margin:0; }
+.wpsg-pv :where(.wpsg-pv__info) { flex:1 1 auto; margin:0; font-size:var(--wpsg-font-size-small); opacity:var(--wpsg-muted-opacity); }
+.wpsg-pv :where(.wpsg-pv__order) { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; margin:0; }
+.wpsg-pv :where(.wpsg-pv__order label) { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; }
+.wpsg-pv :where(.wpsg-pv__modes) { display:flex; gap:0.75em; }
+.wpsg-pv :where(.wpsg-pv__modes a.active) { font-weight:bold; text-decoration:none; }
+.wpsg-pv :where(.wpsg-pv__items) { display:grid; grid-template-columns:repeat(var(--wpsg-columns, 4), minmax(0, 1fr)); gap:calc(var(--wpsg-gap) * 2) var(--wpsg-gap); list-style:none; margin:0; padding:0; }
+.wpsg-pv :where(.wpsg-pv__item) { display:flex; flex-direction:column; gap:0.5em; min-width:0; margin:0; padding:0; }
+.wpsg-pv :where(.wpsg-pv__image img) { display:block; width:100%; height:auto; }
+.wpsg-pv :where(.wpsg-pv__body) { display:flex; flex-direction:column; gap:0.35em; flex:1 1 auto; min-width:0; }
+.wpsg-pv :where(.wpsg-pv__body > p, .wpsg-pv__description) { margin:0; }
+.wpsg-pv :where(.wpsg-pv__name) { margin:0; font-size:1em; line-height:1.3; overflow-wrap:anywhere; hyphens:auto; }
+.wpsg-pv :where(.wpsg-pv__price) { font-size:1.2em; }
+.wpsg-pv :where(.wpsg-pv__price-old) { margin-left:0.5em; font-size:0.8em; opacity:var(--wpsg-muted-opacity); }
+.wpsg-pv :where(.wpsg-pv__detailname, .wpsg-pv__taxnote, .wpsg-pv__baseprice, .wpsg-pv__stock, .wpsg-pv__deliverytime, .wpsg-pv__weight) { font-size:var(--wpsg-font-size-small); opacity:var(--wpsg-muted-opacity); }
+.wpsg-pv :where(.wpsg-pv__description) { display:none; }
+.wpsg-pv :where(.wpsg-pv__action) { display:flex; flex-direction:column; gap:0.5em; margin-top:auto; padding-top:0.5em; }
+.wpsg-pv :where(.wpsg-pv__action form) { margin:0; }
+.wpsg-pv :where(.wpsg-pv__action .wpsg_button) { display:block; width:100%; margin:0; text-align:center; box-sizing:border-box; }
+.wpsg-pv--list :where(.wpsg-pv__items) { grid-template-columns:minmax(0, 1fr); }
+.wpsg-pv--list :where(.wpsg-pv__item) { flex-direction:row; gap:var(--wpsg-gap); padding-bottom:var(--wpsg-gap); border-bottom:1px solid var(--wpsg-color-line-soft); }
+.wpsg-pv--list :where(.wpsg-pv__image) { flex:0 0 min(30%, 220px); }
+.wpsg-pv--list :where(.wpsg-pv__description) { display:block; }
+.wpsg-pv--list :where(.wpsg-pv__action .wpsg_button) { display:inline-block; width:auto; }
+.wpsg-pv--list :where(.wpsg-pv__action) { flex-direction:row; flex-wrap:wrap; }
+.wpsg-pv :where(.wpsg-pv__pager) { display:flex; flex-wrap:wrap; justify-content:center; gap:0.5em; margin-top:calc(var(--wpsg-gap) * 2); }
+.wpsg-pv :where(.wpsg-pv__pager a, .wpsg-pv__pager span) { min-width:2em; padding:0.25em 0.5em; text-align:center; border:1px solid var(--wpsg-color-line); border-radius:var(--wpsg-radius); }
+.wpsg-pv :where(.wpsg-pv__pager span[aria-current]) { font-weight:bold; background-color:var(--wpsg-color-surface); }
+.wpsg-pv :where(.wpsg-pv__pager .wpsg-pv__gap) { border-color:transparent; }
+.wpsg-pv.is-loading { opacity:0.5; pointer-events:none; }
+.wpsg-pvf :where(.wpsg-pvf__item) { margin:0 0 var(--wpsg-gap) 0; padding:0; border:0; min-width:0; }
+.wpsg-pvf :where(.wpsg-pvf__title) { margin:0 0 0.5em 0; padding:0; font-size:1em; font-weight:bold; }
+.wpsg-pvf :where(.wpsg-pvf__item ul) { margin:0; padding-left:1.2em; }
+.wpsg-pvf :where(.wpsg-pvf__price) { display:flex; flex-wrap:wrap; align-items:center; gap:0.5em; }
+.wpsg-pvf :where(.wpsg-pvf__price label) { display:flex; align-items:center; gap:0.35em; }
+.wpsg-pvf :where(.wpsg-pvf__price input) { width:7em; }
+.wpsg-pvf :where(.wpsg-pvf__option) { display:flex; align-items:center; gap:0.5em; margin:0.25em 0; }
+.wpsg-pvf :where(.wpsg-pvf__color) { display:inline-block; width:1.2em; height:1.2em; border:1px solid var(--wpsg-color-line); border-radius:var(--wpsg-radius); }
+.wpsg-pvf :where(.wpsg-pvf__actions) { display:flex; flex-wrap:wrap; align-items:center; gap:var(--wpsg-gap); }
+
/*
* Aufbau mit Klassen-Spezifität: Ausblenden, Ein- und Überblendungen. Allgemeine Regeln der Themes wie
* form input[type=radio] { display:inline-block } dürfen versteckte Bausteine nicht einblenden.
@@ -611,6 +653,10 @@ html body .wpsg_product_garan_wrap a.wpsg_product_garan img { display:block; max
/* Schmale Breiten */
@media (max-width:600px) {
+ .wpsg-pv :where(.wpsg-pv__items) { grid-template-columns:repeat(min(var(--wpsg-columns, 4), 2), minmax(0, 1fr)); }
+ .wpsg-pv--list :where(.wpsg-pv__items) { grid-template-columns:minmax(0, 1fr); }
+ .wpsg-pv--list :where(.wpsg-pv__item) { flex-direction:column; }
+
.wpsg-block-product-list :where(.wpsg-product-list__items) { grid-template-columns:repeat(min(var(--wpsg-columns, 3), 2), minmax(0, 1fr)); }
.wpsg_mod_productindex_grid :where(.wpsg_mod_productindex_product) { width:50%; }
diff --git a/views/mods/mod_productview/productview.js b/views/mods/mod_productview/productview.js
new file mode 100644
index 0000000..5879d68
--- /dev/null
+++ b/views/mods/mod_productview/productview.js
@@ -0,0 +1,148 @@
+/**
+ * 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.
+ */
+
+( 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;
+
+ }
+
+ function refresh( box, change, url ) {
+
+ const field = box.querySelector( 'input[name="wpsg_atts"]' );
+ if ( !field ) return false;
+
+ 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' } )
+ .then( function( r ) { if ( !r.ok ) throw new Error( r.status ); return r.text(); } )
+ .then( function( html ) {
+
+ 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; } );
+ box.replaceWith( fresh );
+ if ( url ) history.pushState( { wpsgPv: true }, '', url );
+
+ } )
+ .catch( function() { if ( url ) 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 applyFilter() {
+
+ const form = document.getElementById( 'wpsg_mod_productview_filter' );
+ const box = document.querySelector( '.wpsg_productview' );
+ if ( !form || !box ) 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 ); } );
+
+ return refresh( box, { filter: filterValues(), page: 1 }, form.action + ( form.action.indexOf( '?' ) === -1 ? '?' : '&' ) + params.toString() );
+
+ }
+
+ // 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; } );
+
+} )();
diff --git a/views/mods/mod_productview/productview.phtml b/views/mods/mod_productview/productview.phtml
new file mode 100644
index 0000000..ae66c39
--- /dev/null
+++ b/views/mods/mod_productview/productview.phtml
@@ -0,0 +1,171 @@
+<?php
+
+ /**
+ * Modul Produktansichten: Ausgabe von [wpsg_productview]
+ *
+ * Das Element .wpsg_productview tauscht productview.js per Ajax aus; das versteckte Feld wpsg_atts mit der Signatur
+ * (_sig) muss in eigenen Fassungen erhalten bleiben. Seiten, Sortierung und Ansicht funktionieren auch ohne JavaScript
+ * über die Adresse (pv-pg, pv-order, pv-mode).
+ */
+
+ if (!defined('ABSPATH')) exit;
+
+ $v = $this->view['wpsg_mod_productview'];
+ $atts = $v['atts'];
+ $currency = $this->get_option('wpsg_currency');
+ $prefix = (($atts['key'] === 'default')?'pv-':'pv-'.$atts['key'].'-');
+
+ $arOrder = $v['arOrder'];
+ if ($atts['order_set'] !== '') $arOrder = array_intersect_key($arOrder, array_flip(explode(',', $atts['order_set'])));
+
+ $hidden = function($name, $value) use (&$hidden) {
+ if (is_array($value)) { foreach ($value as $k => $sub) $hidden($name.'['.$k.']', $sub); return; }
+ echo '<input type="hidden" name="'.esc_attr($name).'" value="'.esc_attr((string)$value).'" />';
+ };
+
+?>
+<div class="wpsg_productview wpsg-pv wpsg-pv--<?php echo esc_attr($v['mode']); ?>" id="wpsg_productview_<?php echo intval($v['index']); ?>" data-wpsg-pv="<?php echo intval($v['index']); ?>" data-wpsg-pv-url="<?php echo esc_url($this->getURL(wpsg_ShopController::URL_BASKET)); ?>">
+
+ <input type="hidden" name="wpsg_atts" value="<?php echo esc_attr((string)wp_json_encode($atts)); ?>" />
+
+ <div class="wpsg-pv__head">
+
+ <?php if ($v['title'] !== '') { ?>
+ <h2 class="wpsg-pv__title"><?php echo esc_html($v['title']); ?></h2>
+ <?php } ?>
+
+ <p class="wpsg-pv__info" aria-live="polite"><?php if ($v['count'] > 0) {
+ echo esc_html(wpsg_translate(__('Anzeige #1# – #2# von #3# Ergebnissen', 'wpsg'), ($v['page'] - 1) * $v['per_page'] + 1, $v['pageMax'], $v['count']));
+ } else {
+ echo esc_html__('Keine Produkte in dieser Ansicht.', 'wpsg');
+ } ?></p>
+
+ <?php if ($atts['hideorder'] !== '1' && $v['count'] > 1 && sizeof($arOrder) > 1) { ?>
+ <form class="wpsg-pv__order" method="get" action="<?php echo esc_url($v['url']); ?>">
+ <?php if ($v['mode'] !== $atts['mode_initial']) $hidden($prefix.'mode', $v['mode']); ?>
+ <?php if (wpsg_isSizedArray($atts['filter'])) $hidden('pvf', $atts['filter']); ?>
+ <label>
+ <span class="wpsg-pv__order-label"><?php echo esc_html__('Sortierung', 'wpsg'); ?></span>
+ <select name="<?php echo esc_attr($prefix.'order'); ?>">
+ <?php foreach ($arOrder as $value => $label) { ?>
+ <option value="<?php echo esc_attr($value); ?>" <?php selected($v['order'], $value); ?>><?php echo esc_html($label); ?></option>
+ <?php } ?>
+ </select>
+ </label>
+ <button type="submit" class="wpsg_button wpsg-pv__order-submit<?php echo wpsg_button_class(); ?>"><?php echo esc_html__('Sortieren', 'wpsg'); ?></button>
+ </form>
+ <?php } ?>
+
+ <?php if ($atts['hideviewselect'] !== '1' && $v['count'] > 0) { ?>
+ <nav class="wpsg-pv__modes" aria-label="<?php echo esc_attr__('Ansicht', 'wpsg'); ?>">
+ <a href="<?php echo esc_url(wpsg_mod_productview::getStateURL($v, ['mode' => 'grid', 'pg' => $v['page']])); ?>" data-mode="grid" <?php echo (($v['mode'] === 'grid')?'aria-current="true" class="active"':''); ?>><?php echo esc_html__('Kacheln', 'wpsg'); ?></a>
+ <a href="<?php echo esc_url(wpsg_mod_productview::getStateURL($v, ['mode' => 'list', 'pg' => $v['page']])); ?>" data-mode="list" <?php echo (($v['mode'] === 'list')?'aria-current="true" class="active"':''); ?>><?php echo esc_html__('Liste', 'wpsg'); ?></a>
+ </nav>
+ <?php } ?>
+
+ </div>
+
+ <?php if (wpsg_isSizedArray($v['products'])) { ?>
+ <ul class="wpsg-pv__items" style="--wpsg-columns:<?php echo intval($v['cols']); ?>">
+ <?php foreach ($v['products'] as $oProduct) { $product_id = intval($oProduct->id); $url = $oProduct->getProductURL(); $name = $oProduct->getProductName(); $attachment_id = $this->imagehandler->getAttachmentID($oProduct->getProductKey()); ?>
+ <li class="wpsg-pv__item wpsg_productview_product_<?php echo $product_id; ?>">
+
+ <?php if (wpsg_isSizedInt($attachment_id)) { ?>
+ <a class="wpsg-pv__image" href="<?php echo esc_url($url); ?>" tabindex="-1" aria-hidden="true"><?php echo wp_get_attachment_image($attachment_id, wpsg_mod_productview::IMAGE_SIZE, false, ['alt' => '']); ?></a>
+ <?php } ?>
+
+ <div class="wpsg-pv__body">
+
+ <h3 class="wpsg-pv__name"><a href="<?php echo esc_url($url); ?>"><?php echo esc_html($name); ?></a></h3>
+ <?php $detailname = (string)$oProduct->detailname; if ($detailname !== '' && $detailname !== $name) { ?>
+ <p class="wpsg-pv__detailname"><?php echo esc_html($detailname); ?></p>
+ <?php } ?>
+
+ <?php $rating = floatval($oProduct->getRating()); if ($rating > 0) { ?>
+ <p class="wpsg-pv__rating" title="<?php echo esc_attr(wpsg_translate(__('#1# von 5 Sternen', 'wpsg'), wpsg_ff($rating, '', true))); ?>"><span aria-hidden="true"><?php echo str_repeat('★', intval(round($rating))).str_repeat('☆', 5 - intval(round($rating))); ?></span><span class="screen-reader-text"><?php echo esc_html(wpsg_translate(__('#1# von 5 Sternen', 'wpsg'), wpsg_ff($rating, '', true))); ?></span></p>
+ <?php } ?>
+
+ <?php if ($oProduct->canDisplayPrice()) { $price = $oProduct->getPrice(); $oldPrice = $oProduct->getOldPrice(); ?>
+ <p class="wpsg-pv__price">
+ <?php if ($oldPrice !== false && $oldPrice != $price) { ?>
+ <span class="wpsg-pv__price-current wpsg_mod_productview_current"><?php echo esc_html(wpsg_ff($price, $currency)); ?></span>
+ <del class="wpsg-pv__price-old wpsg_mod_productview_oldprice"><?php echo esc_html(wpsg_ff($oldPrice, $currency)); ?></del>
+ <?php } else { ?>
+ <span class="wpsg-pv__price-current"><?php echo esc_html(wpsg_ff($price, $currency)); ?></span>
+ <?php } ?>
+ </p>
+ <?php if ($this->hasMod('wpsg_mod_fuellmenge')) { ?>
+ <div class="wpsg-pv__baseprice"><?php echo $this->callMod('wpsg_mod_fuellmenge', 'renderPriceInfo', [$price, $oProduct->fmenge, $product_id]); ?></div>
+ <?php } ?>
+ <p class="wpsg-pv__taxnote wpsg_produkt_preishinweis">
+ <?php $shipping = '<a href="'.esc_url($this->getURL(wpsg_ShopController::URL_VERSANDKOSTEN)).'">'.esc_html__('Versandkosten', 'wpsg').'</a>'; ?>
+ <?php if ($this->get_option('wpsg_kleinunternehmer')) { ?>
+ <?php echo wpsg_translate(esc_html__('Endpreis zzgl. #1#', 'wpsg'), $shipping); ?>
+ <?php } else if ($this->getFrontendTaxview() == WPSG_NETTO) { ?>
+ <?php echo wpsg_translate(esc_html__('(zzgl. #1#% MwSt. und zzgl. #2#)', 'wpsg'), esc_html(wpsg_ff($oProduct->getFrontendTaxValue())), $shipping); ?>
+ <?php } else { ?>
+ <?php echo wpsg_translate(esc_html__('(inkl. #1#% MwSt. und zzgl. #2#)', 'wpsg'), esc_html(wpsg_ff($oProduct->getFrontendTaxValue())), $shipping); ?>
+ <?php } ?>
+ </p>
+ <?php } else { ?>
+ <p class="wpsg-pv__price"><?php echo esc_html__('Preise werden nur für angemeldete Benutzer angezeigt.', 'wpsg'); ?></p>
+ <?php } ?>
+
+ <?php if ($this->hasMod('wpsg_mod_stock') && $this->get_option('wpsg_mod_stock_showProduct') == '1') { ?>
+ <p class="wpsg-pv__stock"><?php echo esc_html(wpsg_translate(__('#1# Artikel auf Lager', 'wpsg'), intval($oProduct->stock))); ?></p>
+ <?php } ?>
+
+ <?php if ($this->hasMod('wpsg_mod_deliverytime') && $this->get_option('wpsg_mod_deliverytime_show_product') == '1') { ?>
+ <p class="wpsg-pv__deliverytime">
+ <?php if ($this->callMod('wpsg_mod_deliverytime', 'isStoreProduct', [$oProduct->getProductKey()])) { ?>
+ <?php echo esc_html((string)$this->callMod('wpsg_mod_deliverytime', 'displayStoreText', [$oProduct->getProductKey()])); ?>
+ <?php } else { $strDelay = (string)$this->callMod('wpsg_mod_deliverytime', 'displayDelayTime', [$oProduct->getProductKey()]); ?>
+ <?php if ($strDelay !== '') { echo esc_html($strDelay); } else { ?>
+ <?php echo esc_html__('Lieferzeit:', 'wpsg'); ?> <?php echo wp_kses_post((string)$this->callMod('wpsg_mod_deliverytime', 'displayDeliveryTime', [$oProduct->getProductKey()])); ?>
+ <?php } ?>
+ <?php } ?>
+ </p>
+ <?php } ?>
+
+ <?php if ($this->hasMod('wpsg_mod_weight') && $this->get_option('wpsg_mod_weight_showProduct') === '1' && floatval($oProduct->weight) > 0) { ?>
+ <p class="wpsg-pv__weight"><?php echo esc_html(wpsg_translate(__('Gewicht: #1#', 'wpsg'), wpsg_ff($oProduct->weight, $this->get_option('wpsg_mod_weight_unit')))); ?></p>
+ <?php } ?>
+
+ <?php $short = (string)$oProduct->getShortDescription(); if ($short !== '') { ?>
+ <div class="wpsg-pv__description"><?php echo wp_kses_post($short); ?></div>
+ <?php } ?>
+
+ <div class="wpsg-pv__action">
+ <?php if ($atts['button'] === '1' && wpsg_buy_in_list($oProduct)) { ?>
+ <form method="post" action="<?php echo esc_url($v['url']); ?>">
+ <input type="hidden" name="wpsg[produkt_id]" value="<?php echo esc_attr((string)$product_id); ?>" />
+ <input type="hidden" name="wpsg[menge]" value="1" />
+ <input type="hidden" name="myReferer" value="<?php echo esc_attr($v['url']); ?>" />
+ <button type="submit" name="wpsg[submit]" value="1" class="wpsg_button wpsg_add_basket<?php echo wpsg_button_class(); ?>"><?php echo esc_html__('In den Warenkorb', 'wpsg'); ?><span class="screen-reader-text"><?php echo esc_html(': '.$name); ?></span></button>
+ </form>
+ <?php } ?>
+ <a class="wpsg_button<?php echo wpsg_button_class(); ?>" href="<?php echo esc_url($url); ?>"><?php echo esc_html__('Zum Produkt', 'wpsg'); ?><span class="screen-reader-text"><?php echo esc_html(': '.$name); ?></span></a>
+ </div>
+
+ </div>
+
+ </li>
+ <?php } ?>
+ </ul>
+ <?php } ?>
+
+ <?php if ($v['pages'] > 1) { ?>
+ <nav class="wpsg-pv__pager wpsg_productview_pager" aria-label="<?php echo esc_attr__('Seiten', 'wpsg'); ?>">
+ <?php for ($i = 1; $i <= $v['pages']; $i ++) { ?>
+ <?php if ($i === $v['page']) { ?>
+ <span aria-current="page"><?php echo $i; ?></span>
+ <?php } else if ($i === 1 || $i === $v['pages'] || abs($i - $v['page']) <= 2) { ?>
+ <a href="<?php echo esc_url(wpsg_mod_productview::getStateURL($v, ['pg' => $i])); ?>" data-page="<?php echo $i; ?>"><?php echo $i; ?></a>
+ <?php } else if (abs($i - $v['page']) === 3) { ?>
+ <span class="wpsg-pv__gap">…</span>
+ <?php } ?>
+ <?php } ?>
+ </nav>
+ <?php } ?>
+
+</div>
diff --git a/views/mods/mod_productview/productview_filter.phtml b/views/mods/mod_productview/productview_filter.phtml
new file mode 100644
index 0000000..1294453
--- /dev/null
+++ b/views/mods/mod_productview/productview_filter.phtml
@@ -0,0 +1,83 @@
+<?php
+
+ /**
+ * Modul Produktansichten: Ausgabe von [wpsg_productview_filter]
+ *
+ * Ein Formular mit GET: Die Filterwerte stehen in der Adresse (pvf[…]), die Seite ist verlinkbar und funktioniert
+ * ohne JavaScript. Mit JavaScript aktualisiert productview.js die Produktansicht ohne Neuladen.
+ */
+
+ if (!defined('ABSPATH')) exit;
+
+ $arValues = $this->view['wpsg_mod_productview_filter']['values'];
+ $currency = $this->get_option('wpsg_currency');
+
+ $checked = function($group, $id, $value) use ($arValues) {
+ $arSet = explode(',', (string)($arValues[$group][$id]??''));
+ return in_array((string)$value, $arSet, true);
+ };
+
+?>
+<form class="wpsg_mod_productview_filter wpsg-pvf" id="wpsg_mod_productview_filter" method="get" action="<?php echo esc_url($this->view['wpsg_mod_productview_filter']['url']); ?>">
+
+ <?php foreach ($this->view['wpsg_mod_productview_filter']['keep'] as $param => $value) { ?>
+ <input type="hidden" name="<?php echo esc_attr($param); ?>" value="<?php echo esc_attr($value); ?>" />
+ <?php } ?>
+
+ <?php foreach ($this->view['showFilter'] as $filter) { ?>
+
+ <?php if ($filter['type'] === 'catfilter') { ?>
+ <nav class="wpsg-pvf__item wpsg-pvf__cat" aria-label="<?php echo esc_attr__('Produktkategorie', 'wpsg'); ?>">
+ <h2 class="wpsg-pvf__title"><?php echo esc_html__('Produktkategorie', 'wpsg'); ?></h2>
+ <ul>
+ <?php foreach ($filter['cat'] as $oCat) { ?>
+ <li><a href="<?php echo esc_url(get_term_link($oCat)); ?>"><?php echo esc_html($oCat->name); ?></a></li>
+ <?php } ?>
+ </ul>
+ </nav>
+
+ <?php } else if ($filter['type'] === 'pricefilter') { ?>
+ <fieldset class="wpsg-pvf__item wpsg-pvf__price">
+ <legend class="wpsg-pvf__title"><?php echo esc_html__('Preis', 'wpsg'); ?></legend>
+ <label><span><?php echo esc_html__('von', 'wpsg'); ?></span> <input type="number" step="0.01" min="0" name="pvf[price_min]" value="<?php echo esc_attr((string)($arValues['price_min']??'')); ?>" placeholder="<?php echo esc_attr(wpsg_ff($filter['min'])); ?>" /></label>
+ <label><span><?php echo esc_html__('bis', 'wpsg'); ?></span> <input type="number" step="0.01" min="0" name="pvf[price_max]" value="<?php echo esc_attr((string)($arValues['price_max']??'')); ?>" placeholder="<?php echo esc_attr(wpsg_ff($filter['max'])); ?>" /></label>
+ <span class="wpsg-pvf__unit"><?php echo esc_html($currency); ?></span>
+ </fieldset>
+
+ <?php } else if ($filter['type'] === 'productvariants') { $variant = $filter['variant']; ?>
+ <fieldset class="wpsg-pvf__item wpsg-pvf__variant wpsg-pvf__variant--<?php echo (($filter['displayMode'] === wpsg_mod_productview::DISPLAYMODE_FILTER_SMALL)?'small':'checkbox'); ?>" data-group="variants" data-id="<?php echo intval($variant['id']); ?>">
+ <legend class="wpsg-pvf__title"><?php echo esc_html($variant['name']); ?></legend>
+ <?php foreach ($filter['variation'] as $variation) { $label = (wpsg_isSizedString($variation['shortname'])?$variation['shortname']:$variation['name']); ?>
+ <label class="wpsg-pvf__option">
+ <input type="checkbox" name="pvf[variants][<?php echo intval($variant['id']); ?>][]" value="<?php echo intval($variation['id']); ?>" <?php checked($checked('variants', intval($variant['id']), intval($variation['id']))); ?> />
+ <?php if (preg_match('/^#[0-9a-fA-F]{3,8}$/', (string)$variation['shortname'])) { ?>
+ <span class="wpsg-pvf__color" style="background-color:<?php echo esc_attr($variation['shortname']); ?>;"></span><span class="screen-reader-text"><?php echo esc_html($variation['name']); ?></span>
+ <?php } else { ?>
+ <span><?php echo esc_html($label); ?></span>
+ <?php } ?>
+ </label>
+ <?php } ?>
+ </fieldset>
+
+ <?php } else if ($filter['type'] === 'productattributs') { $attribute = $filter['attribute']; ?>
+ <fieldset class="wpsg-pvf__item wpsg-pvf__attribute" data-group="attributs" data-id="<?php echo intval($attribute['id']); ?>">
+ <legend class="wpsg-pvf__title"><?php echo esc_html($attribute['name']); ?></legend>
+ <?php foreach ($attribute['auswahl'] as $index => $choice) { ?>
+ <label class="wpsg-pvf__option">
+ <input type="checkbox" name="pvf[attributs][<?php echo intval($attribute['id']); ?>][]" value="<?php echo esc_attr((string)$index); ?>" <?php checked($checked('attributs', intval($attribute['id']), (string)$index)); ?> />
+ <span><?php echo esc_html($choice); ?></span>
+ </label>
+ <?php } ?>
+ </fieldset>
+ <?php } ?>
+
+ <?php } ?>
+
+ <div class="wpsg-pvf__actions">
+ <button type="submit" class="wpsg_button<?php echo wpsg_button_class(); ?>"><?php echo esc_html__('Filtern', 'wpsg'); ?></button>
+ <?php if (wpsg_isSizedArray($arValues)) { ?>
+ <a class="wpsg-pvf__reset" href="<?php echo esc_url($this->view['wpsg_mod_productview_filter']['url']); ?>"><?php echo esc_html__('Filter zurücksetzen', 'wpsg'); ?></a>
+ <?php } ?>
+ </div>
+
+</form>
diff --git a/views/mods/mod_productview/settings_edit.phtml b/views/mods/mod_productview/settings_edit.phtml
new file mode 100644
index 0000000..d2fb02e
--- /dev/null
+++ b/views/mods/mod_productview/settings_edit.phtml
@@ -0,0 +1,10 @@
+<?php
+
+ /**
+ * Modul Produktansichten: Einstellungen (keine; alles über die Attribute der Shortcodes)
+ */
+
+ if (!defined('ABSPATH')) exit;
+
+?>
+<p><?php echo esc_html__('Das Modul hat keine eigenen Einstellungen. Die Produktansicht steuern die Attribute der Shortcodes [wpsg_productview] und [wpsg_productview_filter].', 'wpsg'); ?></p>