File: /home/royaltuning/public_html/public/wp-content/themes/hello-elementor-child/functions.php
<?php
/*
* This is the child theme for Hello Elementor theme, generated with Generate Child Theme plugin by catchthemes.
*
* (Please see https://developer.wordpress.org/themes/advanced-topics/child-themes/#how-to-create-a-child-theme)
*/
add_action( 'wp_enqueue_scripts', 'hello_elementor_child_enqueue_styles' );
function hello_elementor_child_enqueue_styles() {
wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
wp_enqueue_style( 'child-style',
get_stylesheet_directory_uri() . '/style.css',
array('parent-style')
);
}
/*
* Your code goes below
*/
function my_custom_js()
{
echo '<!-- TikTok Pixel Code Start -->
<script>
!function (w, d, t) {
w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=["page","track","identify","instances","debug","on","off","once","ready","alias","group","enableCookie","disableCookie","holdConsent","revokeConsent","grantConsent"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(
var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e},ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};n=document.createElement("script")
;n.type="text/javascript",n.async=!0,n.src=r+"?sdkid="+e+"&lib="+t;e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(n,e)};
ttq.load("CSDOD5BC77U6ERKKAFKG");
ttq.page();
}(window, document, "ttq");
</script>
<!-- TikTok Pixel Code End -->';
}
// Add hook for front-end <head></head>
add_action('wp_head', 'my_custom_js');
// Set billing address fields to not required
add_filter('woocommerce_checkout_fields', 'unrequire_checkout_fields');
function unrequire_checkout_fields($fields)
{
$fields['billing']['billing_company']['required'] = false;
$fields['billing']['billing_city']['required'] = false;
$fields['billing']['billing_postcode']['required'] = false;
$fields['billing']['billing_country']['required'] = false;
$fields['billing']['billing_state']['required'] = false;
$fields['billing']['billing_address_1']['required'] = false;
$fields['billing']['billing_address_2']['required'] = false;
$fields['billing']['billing_phone']['required'] = true;
unset($fields['billing']['billing_address_2']);
return $fields;
}
add_action('wp_footer', 'my_footer_scripts');
function my_footer_scripts()
{
?>
<!-- ÁRUKERESŐ.HU - PLEASE DO NOT MODIFY THE LINES BELOW -->
<script type="text/javascript">
if (ak_widget_params === undefined || ak_widget_script === undefined) {
var ak_widget_params = ["a10f91ce3e92220ea7a29817cd3c58b0", "R", "HU", 0, "W", 0, 480];
var ak_widget_script = document.createElement("script");
ak_widget_script.type = "text/javascript";
ak_widget_script.src = "https://static.arukereso.hu/widget/presenter.js";
ak_widget_script.async = true;
document.body.appendChild(ak_widget_script);
}
</script>
<!-- ÁRUKERESŐ.HU CODE END -->
<script id="barat_hud_sr_script">var hst = document.createElement("script");
hst.src = "//admin.fogyasztobarat.hu/h-api.js";
hst.type = "text/javascript";
hst.setAttribute("data-id", "9JUBDRAA");
hst.setAttribute("id", "fbarat");
var hs = document.getElementById("barat_hud_sr_script");
hs.parentNode.insertBefore(hst, hs);</script>
<?php
}
function load_my_script()
{
wp_register_script(
'my_script',
get_stylesheet_directory_uri() . '/js/custom.js',
array('jquery')
);
wp_enqueue_script('my_script');
}
add_action('wp_enqueue_scripts', 'load_my_script');
function disable_wc_terms_toggle()
{
remove_action("woocommerce_checkout_terms_and_conditions", "wc_terms_and_conditions_page_content", 30);
}
add_action("wp", "disable_wc_terms_toggle");
add_filter('woocommerce_product_tabs', 'my_shipping_tab');
function my_shipping_tab($tabs)
{
// Adds the new tab
$tabs[] = array(
'title' => __('Szállítási & Fizetési információk', 'child-theme'),
'priority' => 50,
'callback' => 'my_shipping_tab_callback'
);
return $tabs;
}
add_action( 'comment_form_after', 'ecommercehints_comment_form_before_fields', 10 );
function ecommercehints_comment_form_before_fields() {
if (is_product()) {
echo "<script defer async src='https://cdn.trustindex.io/loader.js?2d402ea3187026089246be1e473'></script>";
}
}
function my_shipping_tab_callback()
{
// The new tab content
echo '<h2>Szállítási & Fizetési információk</h2>';
echo '<div class="elementor-widget-wrap elementor-element-populated">
<div class="elementor-element elementor-element-3dee91f elementor-widget elementor-widget-heading" data-id="3dee91f" data-element_type="widget" data-widget_type="heading.default">
<div class="elementor-widget-container">
<style>/*! elementor - v3.6.5 - 27-04-2022 */
.elementor-heading-title{padding:0;margin:0;line-height:1}.elementor-widget-heading .elementor-heading-title[class*=elementor-size-]>a{color:inherit;font-size:inherit;line-height:inherit}.elementor-widget-heading .elementor-heading-title.elementor-size-small{font-size:15px}.elementor-widget-heading .elementor-heading-title.elementor-size-medium{font-size:19px}.elementor-widget-heading .elementor-heading-title.elementor-size-large{font-size:29px}.elementor-widget-heading .elementor-heading-title.elementor-size-xl{font-size:39px}.elementor-widget-heading .elementor-heading-title.elementor-size-xxl{font-size:59px}</style><h2 class="elementor-heading-title elementor-size-default">Szállítással kapcsolatos kérdések</h2> </div>
</div>
<div class="elementor-element elementor-element-4af542cc elementor-widget elementor-widget-accordion" data-id="4af542cc" data-element_type="widget" data-widget_type="accordion.default">
<div class="elementor-widget-container">
<style>/*! elementor - v3.6.5 - 27-04-2022 */
.elementor-accordion{text-align:left}.elementor-accordion .elementor-accordion-item{border:1px solid #d4d4d4}.elementor-accordion .elementor-accordion-item+.elementor-accordion-item{border-top:none}.elementor-accordion .elementor-tab-title{margin:0;padding:15px 20px;font-weight:700;line-height:1;cursor:pointer;outline:none}.elementor-accordion .elementor-tab-title .elementor-accordion-icon{display:inline-block;width:1.5em}.elementor-accordion .elementor-tab-title .elementor-accordion-icon svg{width:1em;height:1em}.elementor-accordion .elementor-tab-title .elementor-accordion-icon.elementor-accordion-icon-right{float:right;text-align:right}.elementor-accordion .elementor-tab-title .elementor-accordion-icon.elementor-accordion-icon-left{float:left;text-align:left}.elementor-accordion .elementor-tab-title .elementor-accordion-icon .elementor-accordion-icon-closed{display:block}.elementor-accordion .elementor-tab-title .elementor-accordion-icon .elementor-accordion-icon-opened,.elementor-accordion .elementor-tab-title.elementor-active .elementor-accordion-icon-closed{display:none}.elementor-accordion .elementor-tab-title.elementor-active .elementor-accordion-icon-opened{display:block}.elementor-accordion .elementor-tab-content{display:none;padding:15px 20px;border-top:1px solid #d4d4d4}@media (max-width:767px){.elementor-accordion .elementor-tab-title{padding:12px 15px}.elementor-accordion .elementor-tab-title .elementor-accordion-icon{width:1.2em}.elementor-accordion .elementor-tab-content{padding:7px 15px}}</style> <div class="elementor-accordion" role="tablist">
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1251" class="elementor-tab-title elementor-active" data-tab="1" role="tab" aria-controls="elementor-tab-content-1251" aria-expanded="true" tabindex="0" aria-selected="true">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Mennyi a szállítási idő, Szállitási költség?</a>
</div>
<div id="elementor-tab-content-1251" class="elementor-tab-content elementor-clearfix elementor-active" data-tab="1" role="tabpanel" aria-labelledby="elementor-tab-title-1251" style="display: block;"><p>A szállítási idő <strong>1-2 munkanap</strong>, GLS, Foxpost és Packeta esetében is. A 14:00ig beérkezett rendeléseket még aznap feladjuk. Így az esetek 99%-ban már másnapra kiérkeznek.</p><p><strong>Szállítási költségek: <br></strong>
-GLS házhozszállítás és csomagpont: <em>1499 Ft</em><strong><br></strong>
-Foxpost automata:<strong>: </strong><em>1190 Ft</em><strong><br></strong>
-Packeta csomagpont: <em>1190 Ft</em><br></strong>
-MPL házhozszállítás és csomagpont: <em>1499 Ft</em></p><p>
30.000 ft felett ingyenes a szállítás</p></div>
</div>
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1252" class="elementor-tab-title" data-tab="2" role="tab" aria-controls="elementor-tab-content-1252" aria-expanded="false">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Raktáron vannak-e a termékek vagy rendelni kell?</a>
</div>
<div id="elementor-tab-content-1252" class="elementor-tab-content elementor-clearfix" data-tab="2" role="tabpanel" aria-labelledby="elementor-tab-title-1252"><p>Mindegyik terméknél jelzi automatikusan a rendszer, hogy <strong>készleten van-e</strong> vagy <strong>előrendelhető</strong>. Ha előrendelhető akkor is adható le rendelés, csak várni kell a beszerzésére. Átlagosan 5-7 nap alatt érkeznek meg az “előrendelhető” termékek.</p></div>
</div>
</div>
</div>
</div>
<div class="elementor-element elementor-element-4980b68f elementor-widget elementor-widget-heading" data-id="4980b68f" data-element_type="widget" data-widget_type="heading.default">
<div class="elementor-widget-container">
<h2 class="elementor-heading-title elementor-size-default">Megrendelésse kapcsolatos kérdések</h2> </div>
</div>
<div class="elementor-element elementor-element-46be81be elementor-widget elementor-widget-accordion" data-id="46be81be" data-element_type="widget" data-widget_type="accordion.default">
<div class="elementor-widget-container">
<div class="elementor-accordion" role="tablist">
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1181" class="elementor-tab-title elementor-active" data-tab="1" role="tab" aria-controls="elementor-tab-content-1181" aria-expanded="true" tabindex="0" aria-selected="true">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Kell-e regisztrálni rendeléshez?</a>
</div>
<div id="elementor-tab-content-1181" class="elementor-tab-content elementor-clearfix elementor-active" data-tab="1" role="tabpanel" aria-labelledby="elementor-tab-title-1181" style="display: block;"><p>Nem kötelező, azonban számos előnnyel jár ha készít egy fiókot a rendeléskor.<br>Pár kattintással jóvá lehet hagyni, hogy felhasználói fiókot is készítsen automatikusan a rendszer. A rendelés folyamán a fiókjában tudja követni a csomag állapotát, következő rendeléseknél pedig már el lesznek mentve az adatai .</p></div>
</div>
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1182" class="elementor-tab-title" data-tab="2" role="tab" aria-controls="elementor-tab-content-1182" aria-expanded="false">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Nem vagyok benne biztos hogy melyik alkatrészt rendeljem…ki segít?</a>
</div>
<div id="elementor-tab-content-1182" class="elementor-tab-content elementor-clearfix" data-tab="2" role="tabpanel" aria-labelledby="elementor-tab-title-1182"><p>Ha nem biztos benne, hogy melyik termék lenne önnek megfelelő vegye fel velünk a kapcsolatot a “segíthetünk” gombra kattintva a lap alján és a lehető leghamarabb segítünk a választásban</p></div>
</div>
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1183" class="elementor-tab-title" data-tab="3" role="tab" aria-controls="elementor-tab-content-1183" aria-expanded="false">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Mi a teendő ha mégse jó terméket kaptam?</a>
</div>
<div id="elementor-tab-content-1183" class="elementor-tab-content elementor-clearfix" data-tab="3" role="tabpanel" aria-labelledby="elementor-tab-title-1183"><p>Hibás rendelés esetén vagy ha nem elégedett a termékkel, lehetőség van <strong>cserére </strong>vagy <strong>visszaküldésre</strong>. Mindkét esetben emailben kell jelezni az ügyfélszolgálat felé ezt, és felvesszük veled a kapcsolatot.<br>Email cím: info@royaltuning.hu</p></div>
</div>
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1184" class="elementor-tab-title" data-tab="4" role="tab" aria-controls="elementor-tab-content-1184" aria-expanded="false">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Megbízható a webshop? Mire számíthatok?</a>
</div>
<div id="elementor-tab-content-1184" class="elementor-tab-content elementor-clearfix" data-tab="4" role="tabpanel" aria-labelledby="elementor-tab-title-1184"><p>A webshop megbízhatóságát a független vásárlói visszajelzésekkel szeretnénk átláthatóvá tenni. Ebben az árukereső a partnerünk, akik kérdőiv formájában minden vásárlónktól kérnek visszajelzést. Ezeket a véleményeket bárki elolvashatja akár évekre visszamenőleg is.</p></div>
</div>
</div>
</div>
</div>
<div class="elementor-element elementor-element-4f58bf5c elementor-widget elementor-widget-heading" data-id="4f58bf5c" data-element_type="widget" data-widget_type="heading.default">
<div class="elementor-widget-container">
<h2 class="elementor-heading-title elementor-size-default">Fizetéssel kapcsolatos kérdése</h2> </div>
</div>
<div class="elementor-element elementor-element-733e30ae elementor-widget elementor-widget-accordion" data-id="733e30ae" data-element_type="widget" data-widget_type="accordion.default">
<div class="elementor-widget-container">
<div class="elementor-accordion" role="tablist">
<div class="elementor-accordion-item">
<div id="elementor-tab-title-1931" class="elementor-tab-title elementor-active" data-tab="1" role="tab" aria-controls="elementor-tab-content-1931" aria-expanded="true" tabindex="0" aria-selected="true">
<span class="elementor-accordion-icon elementor-accordion-icon-left" aria-hidden="true">
<span class="elementor-accordion-icon-closed"><i class="fas fa-plus"></i></span>
<span class="elementor-accordion-icon-opened"><i class="fas fa-minus"></i></span>
</span>
<a class="elementor-accordion-title" href="">Milyen fizetési módok vannak az oldalon?</a>
</div>
<div id="elementor-tab-content-1931" class="elementor-tab-content elementor-clearfix elementor-active" data-tab="1" role="tabpanel" aria-labelledby="elementor-tab-title-1931" style="display: block;"><p><strong><em>Utánvétel</em></strong>, tehát amikor átveszed a csomagot akkor fizeted ki. A futároknál lehet kártyával és kézpéznben is fizetni.<br>Foxpost automatánál csak bankkártyával lehet fizetni.</p><p><strong><em>Online bankkártyás fizetés</em></strong>re is van lehetősés <strong>Barion </strong>és <strong>Paylike </strong>fizetőkapun keresztül. Mindkét lehetőség maximális biztonságot nyújt, ezért is választottuk ezt a két céget.</p><p><strong><em>Banki átutalás</em></strong>, ebben az esetben egy bankszámlaszámra kell elutalni a rendelés értékét, és az összeg beérkezése után feladjuk a csomagot.<br>Az átvételkor már nem kell fizetni, csak átvenni a csomagot</p></div>
</div>
</div>
</div>
</div>
</div>';
}
/**
* Add custom tracking code to the thank-you page
*/
add_action('woocommerce_thankyou', 'my_custom_tracking');
function my_custom_tracking($order_id)
{
$order = wc_get_order($order_id);
echo "
<script>
(function(t, r, a, c, k, i, n, g) {t['ROIDataObject'] = k;
t[k]=t[k]||function(){(t[k].q=t[k].q||[]).push(arguments)},t[k].c=i;n=r.createElement(a),
g=r.getElementsByTagName(a)[0];n.async=1;n.src=c;g.parentNode.insertBefore(n,g)
})(window, document, 'script', '//www.arukereso.hu/ocm/sdk.js', 'arukereso', 'hu');
arukereso('authenticate', 'WmyMkg9VCqag5o14bU5Gv9mVMk7caZ6NatAt');
arukereso('set_order_id', " . $order->get_id() . ");";
$line_items = $order->get_items();
foreach ($line_items as $item) {
$product = $order->get_product_from_item($item);
echo "
arukereso('add_product', " . $product->get_sku() . ", '" . $item->get_name() . "', " . $order->get_line_total($item, true, true) . ", " . $item['qty'] . ");";
}
echo "
arukereso('set_total_vat', " . $order->get_total() . ");
arukereso('set_currency', 'HUF');
arukereso('send', 'Order');
</script>";
}
remove_action('shutdown', 'wp_ob_end_flush_all', 1);
add_action('shutdown', function () {
while (@ob_end_flush()) ;
});
function subscribe_link()
{
$servername = "localhost";
$username = "royaltuning_laravel";
$password = "popreg-rixne2-gitqIv";
$dbname = "royaltuning_laravel";
$conn = mysqli_connect($servername, $username, $password, $dbname);
if ($conn->connect_errno) {
echo "Failed to connect to MySQL: " . $conn->connect_error;
exit();
}
printf('Connected successfully.<br />');
/*
$args = array(
'status' => 'publish',
'orderby' => 'title',
'order' => 'DESC',
'limit' => -1,
);
$products = wc_get_products($args);
echo "<pre>";
//var_dump($products);
echo "</pre>";
if (count($products) > 0) {
$k = 0;
foreach ($products as $product) {
$tags = $product->tag_ids;
$id = $product->get_id();
$z = 0;
foreach ($tags as $tag) {
$term = get_term($tag);
$termId=$term->term_id;
$slug=$term->slug;
$name=$term->name;
$desc=$term->description;
$termInsert = "INSERT IGNORE tags(
id, slug, created_at, updated_at) VALUES (
" . $termId . ",'" . $slug . "','" . date("Y-m-d H:i:s") . "','" . date("Y-m-d H:i:s") . "')";
$result2 = mysqli_query($conn, $termInsert);
if (!$result2) {
echo "<p> Query [$termInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
$termInsert2 = "INSERT IGNORE tag_translations(tag_id, locale, name, short_desc, long_desc) VALUES (" . $termId . ",'hu','" . $name . "',NULL,'" . addslashes($desc) . "');";
$result3 = mysqli_query($conn, $termInsert2);
if (!$result3) {
echo "<p> Query [$termInsert2] couldn't be executed </p>";
echo mysqli_error($conn);
}
$termInsert3 = "INSERT IGNORE product_tags(product_id, tag_id) VALUES (" . $id . "," . $termId . ");";
$result4 = mysqli_query($conn, $termInsert3);
if (!$result4) {
echo "<p> Query [$termInsert3] couldn't be executed </p>";
echo mysqli_error($conn);
}
$z++;
}
$k++;
}
}
*/
$categories = get_terms(
array(
'taxonomy' => 'product_cat',
'orderby' => 'name',
'hide_empty' => false,
)
);
//$categories = treeify_terms($categories);
echo "<pre>";
//var_dump($categories);
echo "</pre>";
foreach ($categories as $category) {
$thumbnail_id = get_woocommerce_term_meta($category->term_id, 'thumbnail_id', true);
$imageUrl = wp_get_attachment_url($thumbnail_id);
$id = $category->term_id;
if ($imageUrl) {
$image = file_get_contents($imageUrl);
$extension = pathinfo($imageUrl)['extension'];
$filename = pathinfo($imageUrl)['filename'] . "." . $extension;
$file = "/home/royaltuning/public_html/newshop/public/storage/media/" . $filename;
$mime_type = mime_content_type($file);
$image_size = round(filesize($file));
file_put_contents($file, $image);
$productFeauteredImageInsert = "INSERT IGNORE files (user_id, filename, disk, path, extension, mime, size, created_at) VALUES (1,'" . $filename . "','public_storage','media/" . $filename . "','" . $extension . "','" . $mime_type . "','" . $image_size . "','" . date("Y-m-d H:i:s") . "')";
$image = mysqli_query($conn, $productFeauteredImageInsert);
if (!$image) {
echo "<p> Query [$productFeauteredImageInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
$featuredImageId = $conn->insert_id;
if ($featuredImageId == 0) {
$query = "SELECT * FROM files WHERE filename ='" . $filename . "'";
$result = mysqli_query($conn, $query);
$num_results = mysqli_num_rows($result);
for ($i = 0; $i < $num_results; $i++) {
$row = mysqli_fetch_assoc($result);
$featuredImageId = $row["id"];
}
}
$productFeauteredImageInsert2 = "INSERT IGNORE entity_files (file_id, entity_type, entity_id, zone, created_at) VALUES (" . $featuredImageId . ", '" . addslashes('Modules\Category\Entities\Category') . "', " . $id . ",'logo', '" . date("Y-m-d H:i:s") . "');";
$image2 = mysqli_query($conn, $productFeauteredImageInsert2);
if (!$image2) {
echo "<p> Query [$productFeauteredImageInsert2] couldn't be executed </p>";
echo mysqli_error($conn);
}
}
}
/*
$args = array(
'status' => 'publish',
'orderby' => 'title',
'order' => 'DESC',
'limit' => -1,
);
$products = wc_get_products($args);
echo "<pre>";
//var_dump($products);
if (count($products) > 0) {
$z = 0;
foreach ($products as $product) {
/*
$title = $product->name . " - Royal Tuning Autó és Motoros Kiegészítő Webshop";
$desc = RankMath\Post::get_meta('description', $product->get_id());
$id = $product->get_id();
echo "<br>Meta content:" . $title . "<br>" . $desc;
$metaData = "INSERT IGNORE meta_data (entity_type, entity_id, created_at) VALUES ('" . addslashes('Modules\Product\Entities\Product') . "', " . $id . ", '" . date("Y-m-d H:i:s") . "');";
$image2 = mysqli_query($conn, $metaData);
if (!$image2) {
echo "<p> Query [$metaData] couldn't be executed </p>";
echo mysqli_error($conn);
}
$metaDataId = $conn->insert_id;
$metaDataTrans = "INSERT IGNORE meta_data_translations (meta_data_id, locale, meta_title, meta_description) VALUES (" . $metaDataId . ",'hu','" . $title . "','" . $desc . "');";
$image3 = mysqli_query($conn, $metaDataTrans);
if (!$image3) {
echo "<p> Query [$metaDataTrans] couldn't be executed </p>";
echo mysqli_error($conn);
}
$z++;
$id = $product->get_id();
//if ($id == "8072") {
//var_dump($product);
$imageUrl = wp_get_attachment_url($product->get_image_id());
if ($imageUrl) {
$image = file_get_contents($imageUrl);
$extension = pathinfo($imageUrl)['extension'];
$filename = pathinfo($imageUrl)['filename'] . "." . $extension;
$file = "/home/royaltuning/public_html/newshop/public/storage/media/" . $filename;
$mime_type = mime_content_type($file);
$image_size = round(filesize($file));
file_put_contents($file, $image);
$productFeauteredImageInsert = "INSERT IGNORE files (user_id, filename, disk, path, extension, mime, size, created_at) VALUES (1,'" . $filename . "','public_storage','media/" . $filename . "','" . $extension . "','" . $mime_type . "','" . $image_size . "','" . date("Y-m-d H:i:s") . "')";
$imageQuery = mysqli_query($conn, $productFeauteredImageInsert);
if (!$imageQuery) {
echo "<p> Query [$productFeauteredImageInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
$featuredImageId = $conn->insert_id;
if($featuredImageId==0){
$query = "SELECT * FROM files WHERE filename ='".$filename."'";
$result = mysqli_query($conn, $query);
$num_results = mysqli_num_rows($result);
for($i=0; $i<$num_results; $i++) {
$row = mysqli_fetch_assoc($result);
$featuredImageId =$row["id"];
}
}
$productFeauteredImageInsert2 = "INSERT IGNORE entity_files (file_id, entity_type, entity_id, zone, created_at) VALUES (" . $featuredImageId . ", '" . addslashes('Modules\Product\Entities\Product') . "', " . $id . ",'base_image', '" . date("Y-m-d H:i:s") . "');";
$image2 = mysqli_query($conn, $productFeauteredImageInsert2);
if (!$image2) {
echo "<p> Query [$productFeauteredImageInsert2] couldn't be executed </p>";
echo mysqli_error($conn);
}
} else {
echo $id . "<br>";
}
//$image=$product->get_image_id();
$gallery_images = $product->get_gallery_image_ids();
if (isset($gallery_images[0])) {
foreach ($gallery_images as $gallery_image) {
$imageUrl = wp_get_attachment_url($gallery_image);
if ($imageUrl) {
$image = file_get_contents($imageUrl);
$extension = pathinfo($imageUrl)['extension'];
$filename = pathinfo($imageUrl)['filename'] . "." . $extension;
$file = "/home/royaltuning/public_html/newshop/public/storage/media/" . $filename;
$mime_type = mime_content_type($file);
$image_size = round(filesize($file));
file_put_contents($file, $image);
$productFeauteredImageInsert = "INSERT IGNORE files (user_id, filename, disk, path, extension, mime, size, created_at) VALUES (1,'" . $filename . "','public_storage','media/" . $filename . "','" . $extension . "','" . $mime_type . "','" . $image_size . "','" . date("Y-m-d H:i:s") . "')";
$imageQueryGallery = mysqli_query($conn, $productFeauteredImageInsert);
$featuredImageId = mysqli_insert_id($conn);
if (!$imageQueryGallery) {
echo "<p> Query [$productFeauteredImageInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
if($featuredImageId==0){
$query = "SELECT * FROM files WHERE filename ='".$filename."'";
$result = mysqli_query($conn, $query);
$num_results = mysqli_num_rows($result);
for($i=0; $i<$num_results; $i++) {
$row = mysqli_fetch_assoc($result);
$featuredImageId =$row["id"];
}
}
$productFeauteredImageInsert2 = "INSERT IGNORE entity_files (file_id, entity_type, entity_id, zone, created_at) VALUES (" . $featuredImageId . ", '" . addslashes('Modules\Product\Entities\Product') . "', " . $id . ",'additional_images', '" . date("Y-m-d H:i:s") . "');";
$imageQueryGallery2 = mysqli_query($conn, $productFeauteredImageInsert2);
if (!$imageQueryGallery2) {
echo "<p> Query [$productFeauteredImageInsert2] couldn't be executed </p>";
echo mysqli_error($conn);
}
}
}
} else {
echo $id . "<br>";
}
//return false;
$name = $product->get_name();
$price = $product->get_price();
$sku = $product->get_sku();
$stock = $product->get_stock_quantity() ?? 0;
$slug = $product->get_slug();
$description = $product->get_description();
$short_description = $product->get_short_description();
$categories = $product->get_category_ids();
$productInsert = "INSERT IGNORE products(
id, brand_id, tax_class_id, slug, price, special_price, special_price_type, special_price_start,
special_price_end, selling_price, sku, manage_stock, qty, in_stock, viewed, is_active, new_from,
new_to, deleted_at, created_at, updated_at, is_virtual) VALUES (
" . $id . ",'1','1','" . $slug . "'," . $price . ",NULL,NULL,NULL,NULL," . $price . ",'" . $sku . "',1," . $stock . ",1,0,0,NULL,NULL,NULL,'" . date("Y-m-d H:i:s") . "',NULL,0)";
$result2 = mysqli_query($conn, $productInsert);
if (!$result2) {
echo "<p> Query [$productInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
$productInsert2 = "INSERT IGNORE product_translations(product_id, locale, name, description, short_description) VALUES (" . $id . ",'hu','" . $name . "','" . addslashes($description) . "','" . addslashes($short_description) . "');";
$result3 = mysqli_query($conn, $productInsert2);
if (!$result3) {
echo "<p> Query [$productInsert2] couldn't be executed </p>";
echo mysqli_error($conn);
}
foreach ($categories as $category) {
//var_dump($category);
$categoryInsert = "INSERT IGNORE product_categories (product_id, category_id) VALUES ('$id','$category');";
$result = mysqli_query($conn, $categoryInsert);
if (!$result) {
echo "<p> Query [$categoryInsert] couldn't be executed </p>";
echo mysqli_error($conn);
}
}
//}
}
}
*/
echo "</pre>";
}
add_shortcode('subscribe', 'subscribe_link');
function calculate_delivery_date() {
$now = new DateTime('now', new DateTimeZone('Europe/Budapest'));
$hour = (int) $now->format('H');
$dayOfWeek = (int) $now->format('N'); // 1 = hétfő, 7 = vasárnap
// Alapértelmezett ünnepnapok (2025 Magyarország)
$default_holidays = [
'2025-01-01', '2025-03-15', '2025-04-18', '2025-04-21', '2025-05-01',
'2025-08-20', '2025-10-23', '2025-12-25', '2025-12-26'
];
// Ünnepnapok lekérése az adatbázisból
$custom_holidays = get_option('custom_holidays', []);
if (!is_array($custom_holidays)) {
$custom_holidays = [];
}
$holidays = array_unique(array_merge($default_holidays, $custom_holidays));
// Szállítási dátum számítása
$delivery_date_early = clone $now;
$delivery_date_late = clone $now;
// Péntek 14:00 után, szombat és vasárnap esetén következő kedd a szállítás
if ($dayOfWeek == 6 || $dayOfWeek == 7 || ($dayOfWeek == 5 && $hour >= 14)) {
$delivery_date_early->modify('next tuesday');
} else {
$delivery_date_early->modify('+1 day');
}
// Ha kedd ünnepnap, akkor szerdára módosítjuk
while (in_array($delivery_date_early->format('Y-m-d'), $holidays)) {
$delivery_date_early->modify('+1 day');
}
// Késői szállítás kezelése
if ($dayOfWeek == 6 || $dayOfWeek == 7 || ($dayOfWeek == 5 && $hour >= 14)) {
$delivery_date_late->modify('next tuesday');
} else {
$delivery_date_late->modify('+2 days');
}
// Ha hétvégére esik, akkor a következő hétfőre állítjuk
if ($delivery_date_early->format('N') >= 6) {
$delivery_date_early->modify('next monday');
}
if ($delivery_date_late->format('N') >= 6) {
$delivery_date_late->modify('next monday');
}
// Ha kedd ünnepnap, akkor szerdára módosítjuk
while (in_array($delivery_date_late->format('Y-m-d'), $holidays)) {
$delivery_date_late->modify('+1 day');
}
return '<b>Várható szállítás:</b> <br>Mai nap 14:00 előtt leadott rendelés esetén: <span style="color:green; font-weight:700">' . $delivery_date_early->format('Y.m.d.') . '</span><br> Mai nap 14:00 után leadott rendelés esetén: <span style="color:green; font-weight:700">' . $delivery_date_late->format('Y.m.d.').'</span>';
}
function shipping_eta_shortcode() {
global $post;
$product_id = isset($atts['product_id']) ? intval($atts['product_id']) : 0;
if ($product_id === 0 && isset($post->ID)) {
$product_id = get_the_ID();
}
if ($product_id > 0) {
$productStatus = get_post_meta( $product_id, '_stock_status', true );
if (!$productStatus || $productStatus!="instock") {
return ''; // Ha nincs készleten, nem jelenik meg semmi
}
}
return calculate_delivery_date();
}
add_shortcode('shipping_eta', 'shipping_eta_shortcode');
// Admin felület az ünnepnapok szerkesztéséhez
function shipping_eta_admin_menu() {
add_options_page('Ünnepnapok beállításai', 'Ünnepnapok', 'manage_options', 'shipping_eta_settings', 'shipping_eta_settings_page');
}
add_action('admin_menu', 'shipping_eta_admin_menu');
function shipping_eta_settings_page() {
if (isset($_POST['custom_holidays'])) {
$custom_holidays = array_filter(array_map('sanitize_text_field', $_POST['custom_holidays']));
update_option('custom_holidays', $custom_holidays);
echo '<div class="updated"><p>Ünnepnapok frissítve!</p></div>';
}
$custom_holidays = get_option('custom_holidays', []);
echo '<div class="wrap">';
echo '<h1>Ünnepnapok beállítása</h1>';
echo '<form method="post">';
echo '<div id="holiday-fields">';
$default_holidays = [
'2025-01-01', '2025-03-15', '2025-04-18', '2025-04-21', '2025-05-01',
'2025-08-20', '2025-10-23', '2025-12-25', '2025-12-26'
];
// Alapértelmezett ünnepnapok listázása törölhetően
foreach ($default_holidays as $holiday) {
echo '<div class="holiday-item"><input type="text" name="custom_holidays[]" value="' . esc_attr($holiday) . '" placeholder="YYYY-MM-DD" readonly /> <button type="button" class="remove-holiday button">Törlés</button></div>';
}
// Egyedi ünnepnapok listázása törölhetően
foreach ($custom_holidays as $holiday) {
echo '<div class="holiday-item"><input type="text" name="custom_holidays[]" value="' . esc_attr($holiday) . '" placeholder="YYYY-MM-DD" /> <button type="button" class="remove-holiday button">Törlés</button></div>';
}
echo '</div>';
echo '<button type="button" id="add-holiday" class="button">Új ünnepnap hozzáadása</button><br><br>';
echo '<input type="submit" value="Mentés" class="button button-primary">';
echo '</form>';
echo '</div>';
echo '<script>
document.getElementById("add-holiday").addEventListener("click", function() {
var container = document.getElementById("holiday-fields");
var div = document.createElement("div");
div.className = "holiday-item";
var input = document.createElement("input");
input.type = "text";
input.name = "custom_holidays[]";
input.placeholder = "YYYY-MM-DD";
var button = document.createElement("button");
button.type = "button";
button.className = "remove-holiday button";
button.textContent = "Törlés";
button.onclick = function() { div.remove(); };
div.appendChild(input);
div.appendChild(button);
container.appendChild(div);
});
document.querySelectorAll(".remove-holiday").forEach(button => {
button.addEventListener("click", function() {
this.parentElement.remove();
});
});
</script>';
}
function custom_variation_redirect_add_to_cart($link, $product, $args) {
if ($product->is_type('variable')) {
$link = sprintf(
'<a href="%s" class="button %s">%s</a>',
esc_url(get_permalink($product->get_id())),
esc_attr(isset($args['class']) ? $args['class'] : 'button'),
esc_html__('Válassz opciót', 'woocommerce')
);
}
return $link;
}
add_filter('woocommerce_loop_add_to_cart_link', 'custom_variation_redirect_add_to_cart', 10, 3);
add_action('woocommerce_product_options_general_product_data', 'custom_woo_add_youtube_field');
function custom_woo_add_youtube_field() {
echo '<div class="options_group">';
woocommerce_wp_text_input(array(
'id' => '_youtube_video_url',
'label' => __('YouTube Video URL', 'woocommerce'),
'placeholder' => 'https://www.youtube.com/watch?v=xyz',
'desc_tip' => true,
'description' => __('Add YouTube video URL here.', 'woocommerce'),
));
echo '</div>';
}
// 2. Egyedi mező mentése
add_action('woocommerce_process_product_meta', 'custom_woo_save_youtube_field');
function custom_woo_save_youtube_field($post_id) {
$youtube_url = isset($_POST['_youtube_video_url']) ? sanitize_text_field($_POST['_youtube_video_url']) : '';
update_post_meta($post_id, '_youtube_video_url', $youtube_url);
}
// 3. YouTube script (FancyBox használata popuphoz)
add_action('wp_enqueue_scripts', 'custom_woo_enqueue_scripts');
function custom_woo_enqueue_scripts() {
if (is_product()) {
wp_enqueue_script('jquery');
wp_enqueue_style('fancybox-css', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css');
wp_enqueue_script('fancybox-js', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js', ['jquery'], null, true);
}
}
add_action('admin_enqueue_scripts', function() {
wp_enqueue_script(
'royaltuning-admin-pointer-patch',
get_stylesheet_directory_uri() . '/js/admin-pointer-patch.js',
['jquery'],
'1.0',
true
);
});
// 1. Oszlop hozzáadása a terméklistához
add_filter('manage_edit-product_columns', function($columns) {
$columns['youtube_check'] = 'YouTube Szinkron';
return $columns;
});
// 2. Oszlop tartalom kitöltése
add_action('manage_product_posts_custom_column', function($column, $post_id) {
if ($column === 'youtube_check') {
$product = wc_get_product($post_id);
$description = $product->get_description();
$meta_url = get_post_meta($post_id, '_youtube_video_url', true);
// YouTube link keresése
$has_youtube = preg_match('/https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/[^\s"]+/', $description);
if ($has_youtube && empty($meta_url)) {
echo '<span style="color: red; font-weight: bold;">⚠️ Hiányzó meta!</span>';
} elseif ($has_youtube) {
echo '<span style="color: green;">✅ Ok</span>';
} else {
echo '<span style="color: gray;">—</span>';
}
}
}, 10, 2);
add_filter('woocommerce_product_tabs', 'remove_reviews_from_tabs', 98);
function remove_reviews_from_tabs($tabs) {
unset($tabs['reviews']);
return $tabs;
}
add_shortcode('product_reviews', function() {
if (comments_open()) {
ob_start();
comments_template();
echo " <style>
#reviews{
border:2px solid #34A853 !important;
border-radius:5px;
padding:10px;
}
</style>";
return ob_get_clean();
}
});
add_action('init', function () {
$order_id = 300276; // a rendelés ID-je
$order = wc_get_order($order_id);
if ($order) {
$order->set_currency('HUF');
$order->save();
}
});
//Sociallion codes
add_shortcode( 'al_subcat_grid', function( $atts = [] ) {
if ( ! class_exists( 'WooCommerce' ) ) return '';
// Attributes
$atts = shortcode_atts( [
'cols_desktop' => '4', // 1–6
'cols_tablet' => '3', // 1–6
'cols_mobile' => '2', // 1–6
'gap' => '16', // 8,12,16,20,24,32 (px)
'hide_empty' => 'yes',
'show_on_shop' => 'yes',
'sort_by' => 'title', // title|count|id|slug|term_order|menu_order
'sort_order' => 'ASC', // ASC|DESC
'include' => '', // comma-separated term IDs
'exclude' => '', // comma-separated term IDs
'limit' => '', // integer
'show_count' => 'yes', // yes|no
'img_size' => 'woocommerce_thumbnail', // any registered image size
'cache' => 'yes', // yes|no
], $atts, 'al_subcat_grid' );
// Sanitize + clamp
$clamp = function( $n ){ $n = (int) $n; return max( 1, min( 6, $n ) ); };
$cols_mobile = $clamp( $atts['cols_mobile'] );
$cols_tablet = $clamp( $atts['cols_tablet'] );
$cols_desktop = $clamp( $atts['cols_desktop'] );
$allowed_gaps = [ 8, 12, 16, 20, 24, 32 ];
$gap = (int) $atts['gap'];
if ( ! in_array( $gap, $allowed_gaps, true ) ) $gap = 16;
$hide_empty = strtolower( $atts['hide_empty'] ) === 'yes';
$show_on_shop = strtolower( $atts['show_on_shop'] ) === 'yes';
$show_count = strtolower( $atts['show_count'] ) === 'yes';
$use_cache = strtolower( $atts['cache'] ) === 'yes';
$orderby_in = strtolower( $atts['sort_by'] );
$orderby = in_array( $orderby_in, [ 'title','count','id','slug','term_order','menu_order' ], true ) ? $orderby_in : 'title';
if ( $orderby === 'term_order' ) $orderby = 'menu_order';
$order = strtoupper( $atts['sort_order'] ) === 'DESC' ? 'DESC' : 'ASC';
// Validate img_size against registered sizes; fallback to 'woocommerce_thumbnail'
$img_size = is_string( $atts['img_size'] ) && $atts['img_size'] !== '' ? $atts['img_size'] : 'woocommerce_thumbnail';
$registered_sizes = wp_get_registered_image_subsizes();
if ( $img_size && ! isset( $registered_sizes[ $img_size ] ) ) {
// allow numeric WxH like "300x300"
if ( preg_match( '/^\d+x\d+$/', $img_size ) ) {
// keep custom size string for sizes attr but wp_get_attachment_image requires name or array(width,height)
list($w, $h) = array_map( 'intval', explode( 'x', $img_size ) );
$img_size = [ max(1,$w), max(1,$h) ];
} else {
$img_size = 'woocommerce_thumbnail';
}
}
$taxonomy = 'product_cat';
$parent_term_id = 0;
$q = get_queried_object();
if ( $q && isset( $q->taxonomy ) && $q->taxonomy === $taxonomy ) {
$parent_term_id = (int) $q->term_id;
} elseif ( function_exists( 'is_shop' ) && is_shop() && $show_on_shop ) {
$parent_term_id = 0; // top-level
} else {
return '';
}
// Build get_terms args
$args = [
'taxonomy' => $taxonomy,
'parent' => $parent_term_id,
'hide_empty' => $hide_empty,
'orderby' => $orderby,
'order' => $order,
'fields' => 'all',
];
// Include / exclude
if ( trim( $atts['include'] ) !== '' ) {
$include = array_filter( array_map( 'absint', explode( ',', $atts['include'] ) ) );
if ( $include ) {
$args['include'] = $include;
}
}
if ( trim( $atts['exclude'] ) !== '' ) {
$exclude = array_filter( array_map( 'absint', explode( ',', $atts['exclude'] ) ) );
if ( $exclude ) $args['exclude'] = $exclude;
}
if ( is_numeric( $atts['limit'] ) && (int) $atts['limit'] > 0 ) {
$args['number'] = (int) $atts['limit'];
}
$args = apply_filters( 'al_subcat_grid/get_terms_args', $args, $atts );
// Transient cache
$cache_key = '';
if ( $use_cache ) {
$cache_key = 'al_subcat_grid_' . md5( $parent_term_id . '|' . wp_json_encode( $args ) );
$terms = get_transient( $cache_key );
} else {
$terms = false;
}
if ( false === $terms ) {
$terms = get_terms( $args );
if ( $use_cache && ! is_wp_error( $terms ) ) {
set_transient( $cache_key, $terms, HOUR_IN_SECONDS );
}
}
if ( is_wp_error( $terms ) || empty( $terms ) ) return '';
// Output CSS once
static $css_done = false;
ob_start();
if ( ! $css_done ) : $css_done = true; ?>
<style>
</style>
<?php endif; ?>
<div class="al-subcat-grid" role="region" aria-label="<?php echo esc_attr( sprintf( __( 'Subcategories of %s', 'your-textdomain' ), $parent_term_id ? get_term( $parent_term_id )->name : __( 'Shop', 'your-textdomain' ) ) ); ?>">
<div class="al-grid <?php echo esc_attr( 'm-cols-' . $cols_mobile . ' t-cols-' . $cols_tablet . ' d-cols-' . $cols_desktop . ' gap-' . $gap ); ?>">
<?php foreach ( $terms as $term ) :
$term_link = get_term_link( $term, $taxonomy );
if ( is_wp_error( $term_link ) ) continue;
$thumb_id = (int) get_term_meta( $term->term_id, 'thumbnail_id', true );
$img_html = '';
if ( $thumb_id ) {
$img_html = wp_get_attachment_image(
$thumb_id,
$img_size,
false,
[
'class' => 'al-thumb',
'alt' => $term->name,
'loading' => 'lazy',
'decoding' => 'async',
]
);
} elseif ( function_exists( 'wc_placeholder_img_src' ) ) {
// Respect requested size when possible; wc_placeholder_img_src only takes a name, so fallback to default
$placeholder_src = function_exists( 'wc_placeholder_img_src' ) ? wc_placeholder_img_src( is_string($img_size) ? $img_size : 'woocommerce_thumbnail' ) : '';
$img_html = $placeholder_src ? sprintf(
'<img class="al-thumb" src="%s" alt="%s" loading="lazy" decoding="async" />',
esc_url( $placeholder_src ),
esc_attr( $term->name )
) : '';
}
$count = (int) $term->count;
?>
<a class="al-card" href="<?php echo esc_url( $term_link ); ?>" aria-label="<?php echo esc_attr( $term->name ); ?>">
<?php if ( $img_html ) echo $img_html; ?>
<div class="al-content">
<h3 class="al-title"><?php echo esc_html( $term->name ); ?></h3>
<?php if ( $show_count && $count > 0 ) : ?>
<span class="al-count" aria-label="<?php echo esc_attr( sprintf( _n( '%s product', '%s products', $count, 'your-textdomain' ), number_format_i18n( $count ) ) ); ?>">
<?php echo esc_html( number_format_i18n( $count ) ); ?>
</span>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php
$html = ob_get_clean();
return $html;
} );
/**
* Plugin Name: AL — Woo + Elementor Product Filter Bar (Multi-Grid Scoped + AJAX Rehydrate + Modern Preloader)
* Description: Filter bar for Woo/Elementor Loop Grid. Per-instance namespacing so multiple filter bars & grids on the same page work independently. Archive-aware, preserves params, rehydrates Elementor, accessible UI. Includes optional "source" (e.g. sale) and per-QueryID default source mapping for first-paint correctness. Brand logos, empty-state messages, and performance/a11y improvements included.
* Version: 1.8.4
* Author: Art & Living
* Text Domain: al-woo-ele-filter
*/
if ( ! defined( 'ABSPATH' ) ) exit;
/** =========================
* Dynamic Elementor Query ID registry
* ========================= */
if ( ! function_exists( 'al_pf_register_elementor_query_id' ) ) {
function al_pf_register_elementor_query_id( $ids ) {
static $registered = array();
$ids = is_array( $ids ) ? $ids : array( $ids );
foreach ( $ids as $raw ) {
$id = sanitize_key( $raw );
if ( ! $id || isset( $registered[ $id ] ) ) continue;
add_action( "elementor/query/{$id}", 'al_pf_elementor_posts_query', 5 );
add_action( "elementor_pro/posts/query/{$id}", 'al_pf_elementor_posts_query', 5 );
$registered[ $id ] = true;
}
}
}
// Defaults for compatibility (+ pre-register sale_query_id to ensure early hook availability)
al_pf_register_elementor_query_id( array( 'archive_loop_grid', 'alpf', 'sale_query_id' ) );
/** =========================
* Param helpers (namespaced)
* ========================= */
if ( ! function_exists( 'alpf_get_param' ) ) {
function alpf_get_param( $key, $ns = '' ) {
$key = (string) $key;
$ns = $ns ? sanitize_key( $ns ) : '';
if ( $ns ) {
$nk = "{$ns}_{$key}";
if ( isset( $_GET[ $nk ] ) && $_GET[ $nk ] !== '' ) {
return $_GET[ $nk ];
}
}
return isset( $_GET[ $key ] ) ? $_GET[ $key ] : null;
}
}
if ( ! function_exists( 'alpf_get_param_list' ) ) {
function alpf_get_param_list( $key, $ns = '' ) {
$val = alpf_get_param( $key, $ns );
if ( $val === null || $val === '' ) return array();
$arr = array_map( 'trim', explode( ',', (string) wp_unslash( $val ) ) );
return array_values( array_filter( $arr, 'strlen' ) );
}
}
/** =========================
* Helpers
* ========================= */
if ( ! function_exists( 'al_pf_get_brand_taxonomy' ) ) {
function al_pf_get_brand_taxonomy( $preferred = '' ) {
$preferred = $preferred ? sanitize_key( $preferred ) : '';
if ( $preferred && taxonomy_exists( $preferred ) ) return $preferred;
if ( taxonomy_exists( 'product_brand' ) ) return 'product_brand';
if ( taxonomy_exists( 'yith_product_brand' ) ) return 'yith_product_brand';
if ( taxonomy_exists( 'pa_brand' ) ) return 'pa_brand';
return '';
}
}
/** Detect namespaced/un-namespaced PF params anywhere in query string */
if ( ! function_exists( 'alpf_request_has_pf_params_globally' ) ) {
function alpf_request_has_pf_params_globally() {
foreach ( (array) $_GET as $k => $v ) {
if ( ! is_string( $k ) ) continue;
if ( preg_match( '/(?:^|_)pf_(?:cat|brand|attr|brand_tax|source|min_price|max_price)\z/', $k ) ) {
if ( $v !== '' && $v !== null ) return true;
}
}
return false;
}
}
/** =========================
* Brand/logo helpers
* ========================= */
if ( ! function_exists( 'al_pf_get_term_logo_url' ) ) {
/**
* Resolve a brand term's logo URL using common meta keys.
* Filters:
* - alpf_brand_logo_meta_keys : array of possible meta keys for logo attachment id
* - alpf_brand_logo_url : override the resolved URL
*/
function al_pf_get_term_logo_url( $term_id ) {
$keys = apply_filters( 'alpf_brand_logo_meta_keys', array(
'thumbnail_id',
'brand_thumbnail_id',
'brand_logo_id',
'logo_id',
) );
$url = '';
foreach ( (array) $keys as $meta_key ) {
$att_id = absint( get_term_meta( $term_id, $meta_key, true ) );
if ( $att_id ) {
$src = wp_get_attachment_image_src( $att_id, 'thumbnail' );
if ( $src && ! empty( $src[0] ) ) { $url = $src[0]; break; }
}
}
return (string) apply_filters( 'alpf_brand_logo_url', $url, $term_id );
}
}
/** =========================
* Source handling (sale / all) — URL param or default map
* ========================= */
if ( ! function_exists( 'alpf_get_effective_source' ) ) {
function alpf_get_effective_source( $ns = '' ) {
$ns = $ns ? sanitize_key( $ns ) : '';
$src = alpf_get_param( 'pf_source', $ns );
if ( $src !== null && $src !== '' ) {
return sanitize_key( wp_unslash( $src ) );
}
$map = apply_filters( 'alpf_default_source_map', array() );
if ( is_array( $map ) ) {
if ( $ns && ! empty( $map[ $ns ] ) ) return sanitize_key( $map[ $ns ] );
if ( isset( $map[''] ) && $map[''] ) return sanitize_key( $map[''] );
}
return '';
}
}
if ( ! function_exists( 'al_pf_get_sale_ids' ) ) {
function al_pf_get_sale_ids() {
// Cached for performance on large catalogs
$cache_key = 'alpf_sale_ids_v1';
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) return $cached;
$ids = function_exists( 'wc_get_product_ids_on_sale' ) ? wc_get_product_ids_on_sale() : array();
$ids = is_array( $ids ) ? array_map( 'intval', $ids ) : array();
$ttl = (int) apply_filters( 'alpf_sale_ids_ttl', 5 * MINUTE_IN_SECONDS );
set_transient( $cache_key, $ids, $ttl );
return $ids;
}
}
if ( ! function_exists( 'al_pf_apply_source_to_args' ) ) {
function al_pf_apply_source_to_args( $args, $ns = '' ) {
$src = alpf_get_effective_source( $ns );
if ( $src === 'sale' ) {
$sale_ids = al_pf_get_sale_ids();
if ( ! empty( $sale_ids ) ) {
$args['post__in'] = $sale_ids;
}
}
return $args;
}
}
/** =========================
* Query parts
* ========================= */
if ( ! function_exists( 'al_pf_build_tax_query_parts' ) ) {
function al_pf_build_tax_query_parts( $brand_tax = '', $ns = '' ) {
$parts = array();
$cats_raw = alpf_get_param_list( 'pf_cat', $ns );
if ( $cats_raw ) {
$cats = array_map( 'sanitize_title', $cats_raw );
if ( $cats ) $parts[] = array( 'taxonomy'=>'product_cat', 'field'=>'slug', 'terms'=>$cats, 'operator'=>'IN' );
}
if ( ! $brand_tax ) {
$brand_tax_raw = alpf_get_param( 'pf_brand_tax', $ns );
if ( ! empty( $brand_tax_raw ) ) {
$brand_tax = sanitize_key( wp_unslash( $brand_tax_raw ) );
if ( ! taxonomy_exists( $brand_tax ) ) $brand_tax = '';
}
if ( ! $brand_tax ) $brand_tax = al_pf_get_brand_taxonomy();
}
$brands_raw = alpf_get_param_list( 'pf_brand', $ns );
if ( $brand_tax && $brands_raw ) {
$brands = array_map( 'sanitize_title', $brands_raw );
if ( $brands ) $parts[] = array( 'taxonomy'=>$brand_tax, 'field'=>'slug', 'terms'=>$brands, 'operator'=>'IN' );
}
$attr_raw = alpf_get_param_list( 'pf_attr', $ns );
if ( $attr_raw ) {
$attr_terms_by_tax = array();
foreach ( $attr_raw as $p ) {
if ( strpos( $p, ':' ) === false ) continue;
list( $tax, $slug ) = array_map( 'sanitize_key', explode( ':', $p, 2 ) );
if ( $tax && $slug && taxonomy_exists( $tax ) ) $attr_terms_by_tax[ $tax ][] = $slug;
}
foreach ( $attr_terms_by_tax as $tax => $slugs ) {
$parts[] = array(
'taxonomy' => $tax,
'field' => 'slug',
'terms' => array_values( array_unique( $slugs ) ),
'operator' => 'IN',
);
}
}
return $parts;
}
}
if ( ! function_exists( 'alpf_build_catalog_guards' ) ) {
function alpf_build_catalog_guards( $is_search = false, $ns = '' ) {
$tax_query = array();
$meta_query = array();
$vis_terms = array( 'exclude-from-catalog' );
if ( $is_search ) $vis_terms[] = 'exclude-from-search';
$tax_query[] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => $vis_terms,
'operator' => 'NOT IN',
);
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$meta_query[] = array(
'key' => '_stock_status',
'value' => 'outofstock',
'compare' => '!=',
);
$tax_query[] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => array( 'outofstock' ),
'operator' => 'NOT IN',
);
}
// Safer price bounds
$min_price_raw = alpf_get_param( 'min_price', $ns );
$max_price_raw = alpf_get_param( 'max_price', $ns );
$has_min = ($min_price_raw !== null && $min_price_raw !== '');
$has_max = ($max_price_raw !== null && $max_price_raw !== '');
if ( $has_min || $has_max ) {
$min = $has_min ? max( 0, floatval( wp_unslash( $min_price_raw ) ) ) : 0;
$max = $has_max ? max( $min, floatval( wp_unslash( $max_price_raw ) ) ) : $min;
$meta_query[] = array(
'key' => '_price',
'type' => 'DECIMAL',
'compare' => 'BETWEEN',
'value' => array( (string) $min, (string) $max ),
);
}
return array( $tax_query, $meta_query );
}
}
/** =========================
* Collect terms for UI (respects effective source)
* ========================= */
if ( ! function_exists( 'al_pf_collect_terms_for_filters' ) ) {
function al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, $context = array(), $ns = '' ) {
$s_query = alpf_get_param( 's', $ns );
$s_query = $s_query ? sanitize_text_field( wp_unslash( $s_query ) ) : '';
$pf_cat_s = alpf_get_param( 'pf_cat', $ns );
$pf_brand_s = alpf_get_param( 'pf_brand', $ns );
$pf_attr_s = alpf_get_param( 'pf_attr', $ns );
$ctx_tax = '';
$ctx_terms = array();
if ( empty( $context ) ) {
if ( function_exists( 'is_tax' ) && is_tax() ) {
$qo = get_queried_object();
if ( $qo && ! empty( $qo->taxonomy ) && taxonomy_exists( $qo->taxonomy ) && is_object_in_taxonomy( 'product', $qo->taxonomy ) ) {
$ctx_tax = $qo->taxonomy;
$ctx_terms = array( $qo->slug );
}
}
} else {
$ctx_tax = ! empty( $context['tax'] ) ? sanitize_key( $context['tax'] ) : '';
$ctx_terms = ! empty( $context['terms'] ) ? array_filter( array_map( 'sanitize_title', (array) $context['terms'] ) ) : array();
}
$pf_cats = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $pf_cat_s ) ) ) );
$pf_brands = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $pf_brand_s ) ) ) );
$attr_terms_by_tax = array();
if ( $pf_attr_s ) {
foreach ( array_map( 'trim', explode( ',', (string) $pf_attr_s ) ) as $p ) {
if ( strpos( $p, ':' ) === false ) continue;
list( $tax, $slug ) = array_map( 'sanitize_key', explode( ':', $p, 2 ) );
if ( $tax && $slug && taxonomy_exists( $tax ) ) $attr_terms_by_tax[ $tax ][] = $slug;
}
}
$tax_query = array();
if ( $ctx_tax && $ctx_terms ) {
$tax_query[] = array(
'taxonomy' => $ctx_tax,
'field' => 'slug',
'terms' => $ctx_terms,
'operator' => 'IN',
);
}
if ( $pf_cats ) $tax_query[] = array( 'taxonomy'=>'product_cat', 'field'=>'slug', 'terms'=>$pf_cats, 'operator'=>'IN' );
if ( $brand_tax && $pf_brands ) $tax_query[] = array( 'taxonomy'=>$brand_tax, 'field'=>'slug', 'terms'=>$pf_brands, 'operator'=>'IN' );
if ( $attr_terms_by_tax ) {
foreach ( $attr_terms_by_tax as $tax => $slugs ) {
$tax_query[] = array(
'taxonomy' => $tax,
'field'=>'slug',
'terms' => array_values( array_unique( $slugs ) ),
'operator' => 'IN',
);
}
}
if ( count( $tax_query ) > 1 ) $tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( ! empty( $s_query ), $ns );
$extra_key_bits = (string) apply_filters( 'alpf_transient_key_extra', '' );
$cache_key = 'al_pf_terms_' . md5( wp_json_encode( array(
'ns' => $ns,
's' => $s_query,
'cats' => $pf_cats,
'brand_tax' => $brand_tax,
'brands' => $pf_brands,
'attrs' => $attr_terms_by_tax,
'ctx' => array( 'tax' => $ctx_tax, 'terms' => $ctx_terms ),
'expose' => array_values( array_unique( (array) $attr_tax_slugs ) ),
'source' => (string) alpf_get_effective_source($ns),
'ver' => 'v24-mobile-no-overlay-gap'
) ) . '|' . $extra_key_bits );
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) return $cached;
$final_tax = array_merge( (array) $tax_query, (array) $guard_tax );
if ( count( $final_tax ) > 1 && ! isset( $final_tax['relation'] ) ) {
$final_tax = array_merge( array( 'relation' => 'AND' ), $final_tax );
}
// Optional SKU search (off by default)
$include_sku = (bool) apply_filters( 'alpf_scan_include_sku', false );
$scan_meta = $guard_meta;
if ( $s_query && $include_sku ) {
$scan_meta = array_merge(
array( 'relation' => 'AND' ),
(array) $guard_meta,
array(
array(
'relation' => 'OR',
array(
'key' => '_sku',
'value' => $s_query,
'compare' => 'LIKE',
),
)
)
);
}
$scan_args = array(
'post_type' => 'product',
'post_status' => 'publish',
's' => $s_query,
'fields' => 'ids',
'posts_per_page' => (int) apply_filters( 'al_pf_terms_scan_posts_cap', 5000 ),
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'cache_results' => false,
'orderby' => 'none',
'tax_query' => $final_tax,
'meta_query' => $scan_meta,
);
// Apply "effective source" (e.g., sale) for term scan
$scan_args = al_pf_apply_source_to_args( $scan_args, $ns );
$scan = new WP_Query( $scan_args );
$ids = ! is_wp_error( $scan ) ? array_map( 'intval', $scan->posts ) : array();
$max_ids = (int) apply_filters( 'al_pf_terms_scan_cap', 5000 );
if ( count( $ids ) > $max_ids ) {
$out = array( 'cats'=>array(), 'brands'=>array(), 'attrs'=>array() );
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
$out = array( 'cats' => array(), 'brands' => array(), 'attrs' => array() );
if ( empty( $ids ) ) {
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
/**
* UPDATED BEHAVIOR (v1.8.4):
* - On a parent product_cat archive: show only the direct children that actually have matching products.
* - On a sub-category archive: show only that sub-category itself (no siblings).
* - Outside product_cat contexts: fallback to original "all categories from scanned products".
*/
$cats = array();
$did_scope_to_children = false;
if ( $ctx_tax === 'product_cat' && ! empty( $ctx_terms ) && count( $ctx_terms ) === 1 ) {
$current_term = get_term_by( 'slug', $ctx_terms[0], 'product_cat' );
if ( $current_term && ! is_wp_error( $current_term ) ) {
// SUB-CATEGORY: show only this sub-category name
if ( isset( $current_term->parent ) && (int) $current_term->parent > 0 ) {
$cats = array( $current_term );
$did_scope_to_children = true;
} else {
// PARENT CATEGORY: show only direct children with products
$args = array(
'taxonomy' => 'product_cat',
'parent' => (int) $current_term->term_id,
'orderby' => 'name',
'order' => 'ASC',
'hide_empty' => true,
);
if ( ! empty( $ids ) ) {
$args['object_ids'] = $ids; // bind to matched products
}
$children = get_terms( $args );
if ( ! is_wp_error( $children ) && ! empty( $children ) ) {
$cats = $children;
$did_scope_to_children = true;
} else {
// No children with products -> empty list (UI shows empty-state)
$cats = array();
$did_scope_to_children = true;
}
}
}
}
// Fallback ONLY when not on product_cat context
if ( ! $did_scope_to_children ) {
$cats = wp_get_object_terms( $ids, 'product_cat', array( 'hide_empty' => false ) );
}
if ( ! is_wp_error( $cats ) && $cats ) $out['cats'] = $cats;
if ( $brand_tax ) {
$brands = wp_get_object_terms( $ids, $brand_tax, array( 'hide_empty' => false ) );
if ( ! is_wp_error( $brands ) && $brands ) $out['brands'] = $brands;
}
$attr_tax_slugs = array_values( array_unique( (array) $attr_tax_slugs ) );
foreach ( $attr_tax_slugs as $tax ) {
$terms = wp_get_object_terms( $ids, $tax, array( 'hide_empty' => false ) );
if ( ! is_wp_error( $terms ) && $terms ) $out['attrs'][ $tax ] = $terms;
}
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
}
/** =========================
* Shortcode (UI)
* =========================
* query_id, param_ns, source, label overrides...
*/
if ( ! function_exists( 'al_pf_filter_bar_shortcode' ) ) {
function al_pf_filter_bar_shortcode( $atts ) {
if ( ! class_exists( 'WooCommerce' ) ) return '<!-- WooCommerce not active -->';
$atts = shortcode_atts( array(
'brand_tax' => '',
'attributes' => 'all',
'show_reset' => 'yes',
'target' => '',
'query_id' => '',
'param_ns' => '',
'source' => 'all', // 'all' (default) or 'sale'
'label_category' => '',
'label_brand' => '',
'label_attributes' => '',
'label_reset' => '',
), $atts, 'wc_filter_bar' );
// Register custom Query IDs
$query_ids = array();
if ( ! empty( $atts['query_id'] ) ) {
foreach ( array_map( 'trim', explode( ',', (string) $atts['query_id'] ) ) as $qid ) {
$k = sanitize_key( $qid );
if ( $k ) $query_ids[] = $k;
}
if ( $query_ids ) al_pf_register_elementor_query_id( $query_ids );
}
// Determine namespace for this instance (param_ns > first query_id > '')
$ns = '';
if ( ! empty( $atts['param_ns'] ) ) {
$ns = sanitize_key( $atts['param_ns'] );
} elseif ( ! empty( $query_ids ) ) {
$ns = $query_ids[0];
}
// Auto-target: if no 'target' given but we have a single query_id, assume CSS ID equals query_id
$target_attr = trim( (string) $atts['target'] );
if ( ! $target_attr && ! empty( $query_ids ) ) {
$target_attr = '#' . $query_ids[0];
}
$forced_brand_tax = $atts['brand_tax'] ? sanitize_key( $atts['brand_tax'] ) : '';
$brand_tax = al_pf_get_brand_taxonomy( $forced_brand_tax );
$attr_tax_slugs = array();
if ( 'all' === strtolower( $atts['attributes'] ) ) {
if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
$global_attrs = wc_get_attribute_taxonomies();
if ( $global_attrs ) {
foreach ( $global_attrs as $ga ) {
$tax = 'pa_' . $ga->attribute_name;
if ( taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
}
} else {
foreach ( explode( ',', $atts['attributes'] ) as $maybe ) {
$tax = sanitize_key( trim( $maybe ) );
if ( $tax && taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
$attr_tax_slugs = array_values( array_unique( $attr_tax_slugs ) );
// Context (taxonomy archive)
$ctx_tax = '';
$ctx_terms = array();
if ( function_exists('is_tax') && is_tax() ) {
$qo = get_queried_object();
if ( $qo && ! empty( $qo->taxonomy ) && taxonomy_exists( $qo->taxonomy ) && is_object_in_taxonomy( 'product', $qo->taxonomy ) ) {
$ctx_tax = $qo->taxonomy;
$ctx_terms = array( $qo->slug );
}
}
$present = al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, array(
'tax' => $ctx_tax,
'terms' => $ctx_terms,
), $ns );
$sel_cat_s = alpf_get_param( 'pf_cat', $ns );
$sel_brand_s = alpf_get_param( 'pf_brand',$ns );
$sel_attr_s = alpf_get_param( 'pf_attr', $ns );
$sel_cats = $sel_cat_s ? array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $sel_cat_s ) ) ) ) : array();
$sel_brands = $sel_brand_s? array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $sel_brand_s ) ) ) ) : array();
$sel_attrs = $sel_attr_s ? array_filter( array_map( 'sanitize_key', array_map( 'trim', explode( ',', (string) $sel_attr_s ) ) ) ) : array();
$sel_brand_tax = $brand_tax ? $brand_tax : ( alpf_get_param( 'pf_brand_tax', $ns ) ? sanitize_key( wp_unslash( alpf_get_param( 'pf_brand_tax', $ns ) ) ) : '' );
$search_term = alpf_get_param( 's', $ns );
$search_term = $search_term ? sanitize_text_field( wp_unslash( $search_term ) ) : get_search_query();
$attr_total_terms = 0;
foreach ( (array) $present['attrs'] as $terms ) { if ( is_array( $terms ) ) $attr_total_terms += count( $terms ); }
$instance_id = 'alpf-' . wp_generate_uuid4();
$nonce = wp_create_nonce( 'alpf_terms' );
// ARIA ids
$cats_dd_id = $instance_id . '-cats';
$brand_dd_id = $instance_id . '-brands';
$attrs_dd_id = $instance_id . '-attrs';
// Label overrides (i18n)
$label_category = $atts['label_category'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_category'] ) ) : __( 'Category', 'al-woo-ele-filter' );
$label_brand = $atts['label_brand'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_brand'] ) ) : __( 'Brand', 'al-woo-ele-filter' );
$label_attributes = $atts['label_attributes'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_attributes'] ) ) : __( 'Attributes', 'al-woo-ele-filter' );
$label_reset = $atts['label_reset'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_reset'] ) ) : __( 'Reset', 'al-woo-ele-filter' );
$empty_msg = __( 'Nincs lehetőség a jelenlegi kiválasztáshoz.', 'al-woo-ele-filter' );
// Source handling
$source_attr = strtolower( sanitize_key( $atts['source'] ) );
if ( ! in_array( $source_attr, array( 'all', 'sale' ), true ) ) $source_attr = 'all';
$effective_source = alpf_get_effective_source( $ns );
$resolved_source = ( $source_attr !== 'all' ) ? $source_attr : $effective_source;
ob_start(); ?>
<div class="al-pf-wrap"
data-pf
id="<?php echo esc_attr( $instance_id ); ?>"
data-target="<?php echo esc_attr( $target_attr ); ?>"
data-expose="<?php echo esc_attr( implode( ',', $attr_tax_slugs ) ); ?>"
data-nonce="<?php echo esc_attr( $nonce ); ?>"
data-ctx-tax="<?php echo esc_attr( $ctx_tax ); ?>"
data-ctx-terms="<?php echo esc_attr( implode( ',', $ctx_terms ) ); ?>"
<?php if ( $ns ) : ?> data-ns="<?php echo esc_attr( $ns ); ?>"<?php endif; ?>>
<form class="al-pf-form" onsubmit="return false;" role="search" aria-label="<?php echo esc_attr__( 'Product filters', 'al-woo-ele-filter' ); ?>">
<?php if ( $search_term ) : ?>
<input type="hidden" data-param="s" value="<?php echo esc_attr( $search_term ); ?>">
<?php endif; ?>
<?php if ( $sel_brand_tax ) : ?>
<input type="hidden" data-param="pf_brand_tax" value="<?php echo esc_attr( $sel_brand_tax ); ?>">
<?php endif; ?>
<?php if ( $resolved_source === 'sale' ) : ?>
<input type="hidden" data-param="pf_source" value="sale">
<?php endif; ?>
<!-- Category -->
<details class="al-pf-dd" data-group="pf_cat" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($cats_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_category ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($cats_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_category ); ?></span>
<em class="al-pf-summary" data-summary="pf_cat"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($cats_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_category ); ?>">
<div class="al-pf-empty" data-empty="pf_cat" <?php echo empty($present['cats']) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-menu" <?php echo empty($present['cats']) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php if ( ! empty( $present['cats'] ) ) : foreach ( $present['cats'] as $t ) :
$slug = $t->slug; $checked = in_array( $slug, $sel_cats, true ); ?>
<label class="al-pf-option">
<input type="checkbox" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $checked ); ?> data-param="pf_cat">
<span><?php echo esc_html( $t->name ); ?></span>
</label>
<?php endforeach; endif; ?>
</div>
</div>
</details>
<!-- Brand -->
<?php if ( $sel_brand_tax ) : ?>
<details class="al-pf-dd" data-group="pf_brand" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($brand_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_brand ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($brand_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_brand ); ?></span>
<em class="al-pf-summary" data-summary="pf_brand"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($brand_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_brand ); ?>">
<div class="al-pf-empty" data-empty="pf_brand" <?php echo empty($present['brands']) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-menu" <?php echo empty($present['brands']) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php if ( ! empty( $present['brands'] ) ) : foreach ( $present['brands'] as $b ) :
$slug = $b->slug;
$checked = in_array( $slug, $sel_brands, true );
$logo = al_pf_get_term_logo_url( $b->term_id );
?>
<label class="al-pf-option al-pf-brand-option">
<input type="checkbox" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $checked ); ?> data-param="pf_brand">
<?php if ( $logo ) : ?>
<img class="al-pf-brand-thumb" src="<?php echo esc_url( $logo ); ?>" alt="" loading="lazy" decoding="async">
<?php endif; ?>
<span><?php echo esc_html( $b->name ); ?></span>
</label>
<?php endforeach; endif; ?>
</div>
</div>
</details>
<?php endif; ?>
<!-- Attributes -->
<details class="al-pf-dd" data-group="pf_attr_all" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($attrs_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_attributes ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($attrs_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_attributes ); ?></span>
<em class="al-pf-summary" data-summary="pf_attr_all"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($attrs_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_attributes ); ?>">
<div class="al-pf-empty" data-empty="pf_attr_all" <?php echo ($attr_total_terms===0) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-attr-groups" <?php echo ($attr_total_terms===0) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php foreach ( $present['attrs'] as $tax => $terms ) :
if ( empty( $terms ) ) continue; ?>
<section class="al-pf-attr-group" data-attr-tax="<?php echo esc_attr( $tax ); ?>">
<h4 class="al-pf-group-title"><?php echo esc_html( function_exists('wc_attribute_label') ? wc_attribute_label( $tax ) : $tax ); ?></h4>
<div class="al-pf-grid">
<?php foreach ( $terms as $term ) :
$val = $tax . ':' . $term->slug;
$checked = in_array( $val, $sel_attrs, true ); ?>
<label class="al-pf-option">
<input type="checkbox" value="<?php echo esc_attr( $val ); ?>" <?php checked( $checked ); ?> data-param="pf_attr" data-tax="<?php echo esc_attr( $tax ); ?>">
<span><?php echo esc_html( $term->name ); ?></span>
</label>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
</details>
<?php if ( 'yes' === strtolower( $atts['show_reset'] ) ) : ?>
<button type="button" class="al-pf-reset" aria-label="<?php echo esc_attr( $label_reset ); ?>"><?php echo esc_html( $label_reset ); ?></button>
<?php endif; ?>
</form>
</div>
<style>
/* Add your CSS if needed */
</style>
<script>
(function(){
window.__alpfInstances = window.__alpfInstances || [];
if (!window.CSS || typeof CSS.escape !== 'function') {
window.CSS = window.CSS || {};
CSS.escape = CSS.escape || function(value){ return String(value).replace(/[^a-zA-Z0-9_\\-]/g, '\\\\$&'); };
}
// Simple SR live region for a11y announcements
var __alpfSR = document.getElementById('alpf-sr');
if (!__alpfSR) {
__alpfSR = document.createElement('div');
__alpfSR.id = 'alpf-sr';
__alpfSR.setAttribute('aria-live', 'polite');
__alpfSR.style.position='absolute'; __alpfSR.style.width='1px'; __alpfSR.style.height='1px';
__alpfSR.style.margin='-1px'; __alpfSR.style.border='0'; __alpfSR.style.padding='0';
__alpfSR.style.clip='rect(0 0 0 0)'; __alpfSR.style.overflow='hidden';
document.body.appendChild(__alpfSR);
}
function vw(){ return Math.max(document.documentElement.clientWidth||0, window.innerWidth||0); }
function vh(){ return Math.max(document.documentElement.clientHeight||0, window.innerHeight||0); }
document.querySelectorAll('[data-pf]').forEach(function(root){
if (root.__alpfInit) return;
root.__alpfInit = true;
var targetSelector = root.getAttribute('data-target') || '';
var exposeAttrsCsv = root.getAttribute('data-expose') || '';
var ajaxNonce = root.getAttribute('data-nonce') || '';
var ctxTax = root.getAttribute('data-ctx-tax') || '';
var ctxTermsCsv = root.getAttribute('data-ctx-terms') || '';
var ns = root.getAttribute('data-ns') || ''; // NAMESPACE
var AJAX_URL = '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>';
function nsKey(k){ return ns ? (ns + '_' + k) : k; }
/* UX helpers */
function smoothShow(el){ if(!el) return; el.hidden=false; el.setAttribute('aria-hidden','false'); el.style.opacity='0'; el.style.transform='scale(0.98)'; requestAnimationFrame(function(){ el.style.transition='opacity .18s ease, transform .18s ease'; el.style.opacity='1'; el.style.transform='scale(1)'; }); setTimeout(function(){ el.style.transition=''; el.style.opacity=''; el.style.transform=''; },220); }
function smoothHide(el, after){ if(!el||el.hidden){ if(after) after(); return; } if(el.tagName.toLowerCase()==='details') el.removeAttribute('open'); el.style.transition='opacity .18s ease, transform .18s ease'; el.style.opacity='0'; el.style.transform='scale(0.98)'; setTimeout(function(){ el.hidden=true; el.setAttribute('aria-hidden','true'); el.style.transition=''; el.style.opacity=''; el.style.transform=''; if(after) after(); },200); }
function removeWithFade(el){ if(!el) return; smoothHide(el, function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); }); }
function getCheckedValues(param, scope){
var sel=scope||root;
var inputs=sel.querySelectorAll('input[type="checkbox"][data-param="'+param+'"]:checked');
var vals=[]; inputs.forEach(function(i){ vals.push(i.value); });
return vals;
}
var applyTimer=null; function scheduleApply(){ clearTimeout(applyTimer); applyTimer=setTimeout(function(){ setSummaryText(); applyAjax(true); }, 160); }
function setSummaryText(){
function t(n){return n>0?'('+n+')':'';}
var a=root.querySelector('[data-summary="pf_cat"]'); if(a){a.textContent=t(getCheckedValues('pf_cat').length);}
var b=root.querySelector('[data-summary="pf_brand"]'); if(b){b.textContent=t(getCheckedValues('pf_brand').length);}
var c=root.querySelector('[data-summary="pf_attr_all"]'); if(c){c.textContent=t(getCheckedValues('pf_attr').length);}
}
function applyControlsFromUrl(rootEl, url){
var params=(url instanceof URL)?url.searchParams:new URL(url||window.location.href).searchParams;
function csv(k){ var v=params.get(nsKey(k))||''; return v? v.split(',').map(function(s){ return s.trim(); }).filter(Boolean):[]; }
var cats=csv('pf_cat'), brands=csv('pf_brand'), attrs=csv('pf_attr');
rootEl.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked=false; });
cats.forEach(function(slug){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_cat"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
brands.forEach(function(slug){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_brand"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
attrs.forEach(function(pair){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_attr"][value="'+CSS.escape(pair)+'"]'); if(cb) cb.checked=true; });
}
function buildUrl(baseHref, opts){
opts = opts || {};
var url=new URL(baseHref||window.location.href); var params=url.searchParams;
root.querySelectorAll('[data-param="s"], [data-param="pf_brand_tax"], [data-param="pf_source"]').forEach(function(h){
var key=h.getAttribute('data-param'), val=h.value||'';
val ? params.set(nsKey(key), val) : params.delete(nsKey(key));
});
var cats=getCheckedValues('pf_cat'); cats.length?params.set(nsKey('pf_cat'),cats.join(',')):params.delete(nsKey('pf_cat'));
var br=getCheckedValues('pf_brand'); br.length?params.set(nsKey('pf_brand'),br.join(',')):params.delete(nsKey('pf_brand'));
var at=getCheckedValues('pf_attr'); at.length?params.set(nsKey('pf_attr'),at.join(',')):params.delete(nsKey('pf_attr'));
// Keep orderby if present (namespaced as well)
var curr=new URL(window.location.href);
var ob = curr.searchParams.get(nsKey('orderby'));
if(!params.has(nsKey('orderby')) && ob) params.set(nsKey('orderby'), ob);
if (opts.resetPage) {
['paged','page','product-page'].forEach(function(k){ params.delete(k); });
}
url.search=params.toString(); return url;
}
// ===== Robust widget + container detection =====
function findTargetPair(doc){
var d = doc || document;
var wrapper = null, container = null;
var widgetSelectors = [
'.elementor-widget-loop-grid',
'.elementor-widget-wc-archive-products',
'[data-widget_type*="loop-grid"]',
'[data-element_type="widget"][class*="loop-grid"]'
];
var containerSelectors = [
'.elementor-loop-container',
'.e-loop-container',
'ul.products',
'.woocommerce ul.products',
'.elementor-widget-wc-archive-products .products',
'.woocommerce-products',
'.products',
'.woocommerce .products'
];
function first(scope, sels){
for (var i=0;i<sels.length;i++){
var m = scope.querySelector ? scope.querySelector(sels[i]) : null;
if (m) return m;
}
return null;
}
if (targetSelector) {
var t = d.querySelector(targetSelector);
if (t) {
if (t.matches && (t.matches(widgetSelectors.join(',')))) {
wrapper = t;
} else {
wrapper = first(t, widgetSelectors) || null;
}
if (wrapper) {
container = first(wrapper, containerSelectors) || wrapper.querySelector('ul.products');
} else {
container = first(t, containerSelectors);
}
}
}
if (!wrapper) wrapper = first(d, widgetSelectors);
if (!container) {
if (wrapper) container = first(wrapper, containerSelectors);
if (!container) container = first(d, containerSelectors);
}
return { wrapper: wrapper || null, container: container || null };
}
function setBusy(hostEl, busy){
if (!hostEl) return;
if (!hostEl.classList.contains('alpf-overlay-host')) {
var cs = getComputedStyle(hostEl);
if (cs.position === 'static') hostEl.classList.add('alpf-overlay-host');
}
if (busy) {
if (!hostEl.classList.contains('alpf-target-busy')) {
hostEl.classList.add('alpf-target-busy');
hostEl.setAttribute('aria-busy','true');
var overlay = document.createElement('div');
overlay.className = 'alpf-preloader';
overlay.setAttribute('role','status');
overlay.setAttribute('aria-live','polite');
var label = '<?php echo esc_js( __( 'Loading products…', 'al-woo-ele-filter' ) ); ?>';
overlay.innerHTML = '<div class="alpf-preloader-inner"><div class="alpf-loader" aria-hidden="true"></div><div class="alpf-label">'+ label +'</div></div>';
hostEl.appendChild(overlay);
requestAnimationFrame(function(){ overlay.classList.add('alpf-show'); });
}
} else {
hostEl.classList.remove('alpf-target-busy');
hostEl.removeAttribute('aria-busy');
var overlay = hostEl.querySelector('.alpf-preloader');
if (overlay) { overlay.classList.remove('alpf-show'); setTimeout(function(){ overlay && overlay.remove(); }, 180); }
}
}
function rehydrateScripts(scope){
if(!scope) return;
var scripts = scope.querySelectorAll('script');
scripts.forEach(function(oldS){
var s = document.createElement('script');
for (var i=0;i<oldS.attributes.length;i++) { var a = oldS.attributes[i]; s.setAttribute(a.name, a.value); }
s.textContent = oldS.textContent || '';
oldS.parentNode.replaceChild(s, oldS);
});
}
function rehydrateElementor(scope){
if (window.elementorFrontend) {
try {
var $scope = window.jQuery ? window.jQuery(scope) : null;
if (elementorFrontend.elementsHandler && elementorFrontend.elementsHandler.runReadyTrigger) elementorFrontend.elementsHandler.runReadyTrigger($scope || scope);
if (elementorFrontend.hooks && elementorFrontend.hooks.doAction) {
elementorFrontend.hooks.doAction('frontend/element_ready/global', $scope || scope);
elementorFrontend.hooks.doAction('frontend/element_ready/loop-grid.default', $scope || scope);
elementorFrontend.hooks.doAction('frontend/element_ready/woocommerce-archive-products.default', $scope || scope);
}
if (elementorFrontend.elements && elementorFrontend.elements.$window) elementorFrontend.elements.$window.trigger('resize');
} catch(e){ console.warn('[al-pf] Elementor re-init failed:', e); }
}
}
function setEmptyState(groupKey, hasItems) {
var panel = root.querySelector('[data-group="'+groupKey+'"] .al-pf-panel');
if (!panel) return;
var emptyEl = panel.querySelector('.al-pf-empty[data-empty="'+groupKey+'"]');
var listEl = panel.querySelector(groupKey === 'pf_attr_all' ? '.al-pf-attr-groups' : '.al-pf-menu');
if (emptyEl) {
emptyEl.hidden = !!hasItems;
emptyEl.setAttribute('aria-hidden', hasItems ? 'true' : 'false');
}
if (listEl) {
listEl.hidden = !hasItems;
listEl.setAttribute('aria-hidden', hasItems ? 'false' : 'true');
}
}
function rebuildCheckboxList(container, param, items) {
var selected = new Set(getCheckedValues(param));
container.innerHTML = '';
var hasItems = !!(items && items.length);
if (!hasItems) {
setEmptyState(param, false);
return;
}
items.forEach(function(it){
var label = document.createElement('label');
label.className = 'al-pf-option' + (param === 'pf_brand' ? ' al-pf-brand-option' : '');
var input = document.createElement('input');
input.type = 'checkbox';
input.setAttribute('data-param', param);
input.value = it.slug;
if (selected.has(it.slug)) input.checked = true;
input.addEventListener('change', scheduleApply);
label.appendChild(input);
if (param === 'pf_brand' && it.logo) {
var img = document.createElement('img');
img.className = 'al-pf-brand-thumb';
img.src = it.logo;
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
label.appendChild(img);
}
var span = document.createElement('span');
span.textContent = it.name;
label.appendChild(span);
container.appendChild(label);
});
setEmptyState(param, true);
}
async function refreshFilterOptions(urlObj){
var u = new URL(AJAX_URL, window.location.origin);
var activeUrl = (urlObj instanceof URL) ? urlObj : new URL(window.location.href);
u.search = activeUrl.search;
u.searchParams.set('action','alpf_terms');
if (exposeAttrsCsv) u.searchParams.set('expose_attrs', exposeAttrsCsv);
if (ajaxNonce) u.searchParams.set('security', ajaxNonce);
if (ctxTax) u.searchParams.set('ctx_tax', ctxTax);
if (ctxTermsCsv) u.searchParams.set('ctx_terms', ctxTermsCsv);
if (ns) u.searchParams.set('ns', ns); // pass namespace to server
try{
var res = await fetch(u.toString(), { credentials:'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if (!res.ok) throw new Error('HTTP '+res.status);
var j = await res.json();
if (!j || !j.success) return;
var data = j.data || {};
var attrLabels = data.attr_labels || {};
var catDetails = root.querySelector('[data-group="pf_cat"]');
var catMenu = root.querySelector('[data-group="pf_cat"] .al-pf-menu');
if (catDetails && catMenu) {
rebuildCheckboxList(catMenu, 'pf_cat', data.cats || []);
}
var brandDetails = root.querySelector('[data-group="pf_brand"]');
var brandMenu = root.querySelector('[data-group="pf_brand"] .al-pf-menu');
if (brandDetails && brandMenu) {
rebuildCheckboxList(brandMenu, 'pf_brand', data.brands || []);
}
// Remove empty attribute sections; we still keep the dropdown visible with empty message
Array.from(root.querySelectorAll('.al-pf-attr-group')).forEach(function(sec){
var tax = sec.getAttribute('data-attr-tax') || '';
var items = (data.attrs && data.attrs[tax]) ? data.attrs[tax] : [];
if (!items.length) removeWithFade(sec);
});
function ensureAttrSection(tax, labelText){
var groupsWrap = root.querySelector('.al-pf-attr-groups');
if (!groupsWrap) return null;
var section = root.querySelector('.al-pf-attr-group[data-attr-tax="'+CSS.escape(tax)+'"]');
if (section) return section;
section = document.createElement('section');
section.className = 'al-pf-attr-group';
section.setAttribute('data-attr-tax', tax);
var h4 = document.createElement('h4'); h4.className = 'al-pf-group-title'; h4.textContent = labelText || tax;
var grid = document.createElement('div'); grid.className = 'al-pf-grid';
section.appendChild(h4); section.appendChild(grid);
groupsWrap.appendChild(section);
return section;
}
function rebuildAttrGroup(section, tax, items){
var grid = section.querySelector('.al-pf-grid');
var selected = new Set(getCheckedValues('pf_attr'));
grid.innerHTML = '';
if (!items || !items.length) return;
items.forEach(function(it){
var val = tax + ':' + it.slug;
var label = document.createElement('label'); label.className='al-pf-option';
var input = document.createElement('input');
input.type='checkbox'; input.setAttribute('data-param','pf_attr'); input.setAttribute('data-tax',tax); input.value = val;
if (selected.has(val)) input.checked = true;
input.addEventListener('change', scheduleApply);
var span = document.createElement('span'); span.textContent = it.name;
label.appendChild(input); label.appendChild(span);
grid.appendChild(label);
});
}
(Object.keys(data.attrs||{})).forEach(function(tax){
var items = (data.attrs && data.attrs[tax]) ? data.attrs[tax] : [];
if (!items.length) return;
var labelText = (attrLabels && attrLabels[tax]) ? attrLabels[tax] : tax;
var section = ensureAttrSection(tax, labelText);
if (!section) return;
rebuildAttrGroup(section, tax, items);
section.hidden=false; section.setAttribute('aria-hidden','false');
section.style.opacity='0'; section.style.transform='scale(0.98)';
requestAnimationFrame(function(){
section.style.transition='opacity .18s ease, transform .18s ease';
section.style.opacity='1'; section.style.transform='scale(1)';
setTimeout(function(){ section.style.transition=''; section.style.opacity=''; section.style.transform=''; },220);
});
});
var anyAttrItems = data.attrs && Object.keys(data.attrs).some(function(t){ return (data.attrs[t]||[]).length; });
setEmptyState('pf_attr_all', !!anyAttrItems);
setSummaryText();
} catch(err){ console.warn('[al-pf] terms refresh failed:', err); }
}
async function fetchAndSwap(url){
var cur = findTargetPair(document);
var busyHost = cur.wrapper || cur.container;
if (!busyHost) { window.location.href = url.toString(); return; }
setBusy(busyHost, true);
var safetyTimer = setTimeout(function(){ setBusy(busyHost, false); }, 8000);
try {
var xhrUrl = new URL(url.toString());
xhrUrl.searchParams.set('_alpf', '1');
var res = await fetch(xhrUrl.toString(), {
credentials:'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!res.ok) throw new Error('HTTP '+res.status);
var html = await res.text();
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var neu = findTargetPair(doc);
var newWrapper = neu.wrapper;
var newContainer = neu.container;
if (!newWrapper && !newContainer) throw new Error('Target nodes not found in response');
var didReplaceWrapper = false;
if (cur.wrapper && newWrapper) {
var tmp = document.createElement('div');
tmp.innerHTML = newWrapper.outerHTML;
var fresh = tmp.firstElementChild;
cur.wrapper.replaceWith(fresh);
var updated = findTargetPair(document);
didReplaceWrapper = true;
rehydrateScripts(updated.wrapper || updated.container);
rehydrateElementor(updated.wrapper || updated.container);
bindContainerInteractions(updated.container || updated.wrapper);
setBusy(updated.wrapper || updated.container, false);
} else if (cur.container && newContainer) {
cur.container.innerHTML = newContainer.innerHTML;
rehydrateScripts(cur.container);
rehydrateElementor(cur.container);
bindContainerInteractions(cur.container);
} else {
window.location.href = url.toString();
return;
}
refreshFilterOptions(url);
var region = (didReplaceWrapper ? (findTargetPair(document).wrapper || findTargetPair(document).container) : cur.container);
if (region) {
region.setAttribute('role','region');
region.setAttribute('aria-live','polite');
region.scrollIntoView({ behavior:'smooth', block:'start' });
}
__alpfSR.textContent = '<?php echo esc_js( __( 'Products updated', 'al-woo-ele-filter' ) ); ?>';
document.dispatchEvent(new CustomEvent('alpf:afterSwap', { detail: { container: region, url: url.toString() } }));
if (window.jQuery) {
jQuery(document.body).trigger('alpf_after_swap', [ region, url.toString() ]);
jQuery(document.body).trigger('updated_wc_div');
jQuery(document.body).trigger('wc_fragment_refresh');
jQuery(document.body).trigger('init_price_filter');
jQuery(document.body).trigger('wc_update_cart');
}
} catch(err){
console.error('[al-pf] AJAX swap failed:', err);
window.location.href = url.toString();
} finally {
clearTimeout(safetyTimer);
var latest = findTargetPair(document);
setBusy(latest.wrapper || latest.container || busyHost, false);
}
}
function applyAjax(filtersChanged){
var url = buildUrl(undefined, { resetPage: !!filtersChanged });
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf: 1, href: href }, '', href);
}
fetchAndSwap(url);
if (filtersChanged) closeAll(null);
}
// Close all dropdowns except the provided one
function closeAll(except){
root.querySelectorAll('.al-pf-dd[open]').forEach(function(dd){ if (dd !== except) dd.removeAttribute('open'); });
}
// ===== Mobile viewport clamping / gap fix (no overlay) =====
function fitPanel(dd){
if (!dd) return;
var panel = dd.querySelector('.al-pf-panel'); if (!panel) return;
// Reset positioning
panel.classList.remove('mobile-fit');
panel.style.position=''; panel.style.left=''; panel.style.right=''; panel.style.top=''; panel.style.bottom='';
panel.style.maxHeight=''; panel.style.width=''; panel.style.maxWidth=''; panel.classList.remove('fit-clamped');
dd.classList.remove('flip');
var viewportW = vw();
var viewportH = vh();
var GAP = 10; // ensure there's always a visual gap from the opener
// Small screens: fixed sheet without overlay; clamp and add gap so it never overlaps opener
if (viewportW <= 480) {
var ddRect = dd.getBoundingClientRect();
panel.classList.add('mobile-fit');
panel.style.left = '8px';
panel.style.right = '8px';
panel.style.maxHeight = Math.round(viewportH * 0.7) + 'px';
// Measure current height (after maxHeight applied)
var ph = panel.getBoundingClientRect().height || Math.round(viewportH * 0.7);
var spaceBelow = viewportH - ddRect.bottom - GAP;
var spaceAbove = ddRect.top - GAP;
if (spaceBelow >= Math.min(ph, viewportH * 0.4)) {
// Open below, with gap -> no overlap with opener
var top = Math.min(ddRect.bottom + GAP, viewportH - GAP - Math.min(ph, Math.round(viewportH * 0.7)));
panel.style.top = top + 'px';
} else {
// Open above, with gap
var topAbove = Math.max(GAP, ddRect.top - GAP - Math.min(ph, Math.round(viewportH * 0.7)));
panel.style.top = topAbove + 'px';
}
return;
}
// Desktop/tablet: absolute positioning with clamping and flip
var vwPadding = 12;
var maxW = Math.min(1200, viewportW - (vwPadding*2));
var natural = Math.ceil(panel.scrollWidth + 20);
var minW = Math.max(220, Math.min(340, viewportW - 2*vwPadding));
var finalW = Math.max(minW, Math.min(natural, maxW));
panel.style.maxWidth = Math.round(maxW) + 'px';
panel.style.width = Math.round(finalW) + 'px';
panel.style.left='0px'; panel.style.right='auto';
var rect=panel.getBoundingClientRect();
if (rect.right > viewportW - vwPadding) { panel.style.left='auto'; panel.style.right='0px'; }
var ddRect=dd.getBoundingClientRect(); var spaceBelow=viewportH-ddRect.bottom; var spaceAbove=ddRect.top;
if (spaceBelow < rect.height && spaceAbove > spaceBelow) dd.classList.add('flip');
// After flip, re-measure and correct horizontal overflow
rect = panel.getBoundingClientRect();
if (rect.left < vwPadding) {
panel.style.left = (vwPadding - (ddRect.left)) + 'px';
panel.style.right = 'auto';
}
rect = panel.getBoundingClientRect();
if (rect.right > viewportW - vwPadding) {
panel.style.left = 'auto';
panel.style.right = (vwPadding) + 'px';
}
}
function bindContainerInteractions(container){
if (!container) return;
if (!container.dataset.alpfPagBound) {
container.addEventListener('click', function(e){
var a = e.target.closest('.page-numbers a, a.page-numbers, .woocommerce-pagination a');
if (!a) return;
e.preventDefault();
var url = new URL(a.href);
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
}, { passive:false });
container.dataset.alpfPagBound = '1';
}
var orderForm = container.querySelector('form.woocommerce-ordering');
var select = orderForm ? orderForm.querySelector('select[name="orderby"]') : null;
if (!select) {
var globalForm = document.querySelector('form.woocommerce-ordering');
if (globalForm) select = globalForm.querySelector('select[name="orderby"]');
}
if (select && !select.dataset.alpfBound) {
select.addEventListener('change', function(){
var url = buildUrl(undefined, { resetPage: true });
url.searchParams.set(nsKey('orderby'), select.value); // namespaced orderby
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
});
select.dataset.alpfBound = '1';
}
}
// Toggle behavior + events (enforce single-open)
root.querySelectorAll('.al-pf-dd').forEach(function(dd){
var summary = dd.querySelector('.al-pf-trigger');
// Pre-close siblings BEFORE the browser toggles (pointer/keyboard)
if (summary) {
summary.addEventListener('pointerdown', function(){
if (!dd.open) closeAll(dd);
});
summary.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') {
if (!dd.open) closeAll(dd);
}
});
}
// Fallback when toggle completes
dd.addEventListener('toggle', function(){
if (summary) summary.setAttribute('aria-expanded', dd.open ? 'true' : 'false');
if (dd.open) {
closeAll(dd);
fitPanel(dd);
}
});
// Close on ESC
dd.addEventListener('keydown', function(e){
if (e.key === 'Escape') dd.removeAttribute('open');
});
});
// Refit on resize/orientation/scroll (helps fixed mobile sheet stay aligned)
var refit = function(){
var openDD = root.querySelector('.al-pf-dd[open]');
if (openDD) fitPanel(openDD);
};
window.addEventListener('resize', refit, { passive:true });
window.addEventListener('orientationchange', refit, { passive:true });
window.addEventListener('scroll', function(){
if (document.querySelector('.al-pf-panel.mobile-fit')) refit();
}, { passive:true });
document.addEventListener('click', function(e){
var isInside = e.target.closest ? e.target.closest('.al-pf-dd') : null;
if (isInside && root.contains(isInside)) return;
if (!root.contains(e.target)) closeAll(null);
});
// Change listeners
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.addEventListener('change', scheduleApply); });
// Reset
var resetBtn = root.querySelector('.al-pf-reset');
if (resetBtn){
resetBtn.addEventListener('click', function(){
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked = false; });
setSummaryText();
var url = buildUrl(undefined, { resetPage: true });
url.searchParams.delete(nsKey('orderby'));
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
closeAll(null);
});
}
// Initial
applyControlsFromUrl(root, window.location.href);
var initialPair = findTargetPair(document);
bindContainerInteractions(initialPair.container || initialPair.wrapper);
setSummaryText();
refreshFilterOptions(new URL(window.location.href));
if (initialPair.container || initialPair.wrapper) {
var scope = initialPair.wrapper || initialPair.container;
document.dispatchEvent(new CustomEvent('alpf:afterSwap', { detail: { container: scope, url: window.location.href } }));
if (window.jQuery) jQuery(document.body).trigger('alpf_after_swap', [ scope, window.location.href ]);
}
window.__alpfInstances.push({ root: root, refresh: function(){ var url = new URL(window.location.href); fetchAndSwap(url); } });
});
// History back/forward — honor namespace
window.addEventListener('popstate', function(){
if (!window.__alpfInstances) return;
var url = new URL(window.location.href);
window.__alpfInstances.forEach(function(i){
if (!i || !i.root) return;
var root=i.root, ns=root.getAttribute('data-ns')||'';
function nsKey(k){ return ns ? (ns+'_'+k) : k; }
function csv(k){ var v=url.searchParams.get(nsKey(k))||''; return v? v.split(',').map(function(s){return s.trim();}).filter(Boolean):[]; }
var cats=csv('pf_cat'), brands=csv('pf_brand'), attrs=csv('pf_attr');
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked=false; });
cats.forEach(function(slug){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_cat"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
brands.forEach(function(slug){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_brand"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
attrs.forEach(function(pair){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_attr"][value="'+CSS.escape(pair)+'"]'); if(cb) cb.checked=true; });
function gv(p){ return Array.from(root.querySelectorAll('input[type="checkbox"][data-param="'+p+'"]:checked')).map(function(n){return n.value;}); }
function t(n){return n>0?'('+n+')':'';}
var a=root.querySelector('[data-summary="pf_cat"]'); if(a){a.textContent=t(gv('pf_cat').length);}
var b=root.querySelector('[data-summary="pf_brand"]'); if(b){b.textContent=t(gv('pf_brand').length);}
var c=root.querySelector('[data-summary="pf_attr_all"]'); if(c){c.textContent=t(gv('pf_attr').length);}
if (typeof i.refresh === 'function') i.refresh();
});
});
})();
</script>
<?php
return ob_get_clean();
}
}
add_shortcode( 'wc_filter_bar', 'al_pf_filter_bar_shortcode' );
/** =========================
* Server-side query filters (main query)
* ========================= */
if ( ! function_exists( 'al_pf_filter_main_search_query' ) ) {
function al_pf_filter_main_search_query( $q ) {
if ( is_admin() || ! ( $q instanceof WP_Query ) || ! $q->is_main_query() ) return;
// Detect any namespaced or non-namespaced PF params
$has_pf_params = alpf_request_has_pf_params_globally();
$is_product_archive = function_exists('is_post_type_archive') && is_post_type_archive('product');
$is_product_tax = false;
if ( function_exists('get_object_taxonomies') && ! is_null( get_object_taxonomies( 'product' ) ) ) {
foreach ( get_object_taxonomies( 'product' ) as $ptax ) {
if ( function_exists('is_tax') && is_tax( $ptax ) ) { $is_product_tax = true; break; }
}
}
$is_product_pt = ( 'product' === $q->get('post_type') || ( is_array( $q->get('post_type') ) && in_array( 'product', (array) $q->get('post_type'), true ) ) );
if ( $q->is_search() && ! $has_pf_params && ! $is_product_archive && ! $is_product_tax && ! $is_product_pt ) return;
if ( $q->is_search() && $has_pf_params ) {
$q->set( 'post_type', array( 'product' ) );
}
if ( $has_pf_params || $is_product_archive || $is_product_tax || $is_product_pt ) {
$parts = al_pf_build_tax_query_parts( '', '' );
if ( $parts ) {
$tax_query = (array) $q->get( 'tax_query' );
$tax_query = array_merge( $tax_query, $parts );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
$q->set( 'tax_query', $tax_query );
}
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( $q->is_search(), '' );
$tax_query = array_merge( (array) $q->get('tax_query'), $guard_tax );
$meta_query = array_merge( (array) $q->get('meta_query'), $guard_meta );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query = array_merge( array( 'relation' => 'AND' ), $meta_query );
}
// Apply effective source globally (ns '')
$args = al_pf_apply_source_to_args( array(), '' );
if ( ! empty( $args['post__in'] ) ) {
$existing = (array) $q->get( 'post__in' );
if ( ! empty( $existing ) ) {
$q->set( 'post__in', array_values( array_intersect( array_map( 'intval', $existing ), array_map( 'intval', $args['post__in'] ) ) ) );
} else {
$q->set( 'post__in', $args['post__in'] );
}
}
$q->set( 'tax_query', $tax_query );
$q->set( 'meta_query', $meta_query );
}
}
}
add_action( 'pre_get_posts', 'al_pf_filter_main_search_query' );
/** =========================
* Elementor Loop Grid query — namespaced
* ========================= */
if ( ! function_exists( 'al_pf_elementor_posts_query' ) ) {
function al_pf_elementor_posts_query( $query ) {
if ( is_admin() ) return;
if ( ! ( $query instanceof WP_Query ) ) return;
$hook = current_filter();
$parts = explode( '/', (string) $hook );
$ns = sanitize_key( end( $parts ) );
$default_pp = (int) apply_filters( 'alpf_posts_per_page_default', 30 );
$pp = (int) $query->get( 'posts_per_page' );
if ( $pp < 1 ) $query->set( 'posts_per_page', $default_pp );
// namespaced orderby
$orderby_raw = alpf_get_param( 'orderby', $ns );
if ( $orderby_raw !== null && $orderby_raw !== '' ) {
$orderby = function_exists('wc_clean') ? wc_clean( wp_unslash( $orderby_raw ) ) : sanitize_text_field( wp_unslash( $orderby_raw ) );
al_pf_apply_elementor_orderby_map( $query, $orderby );
}
// Any namespaced filter/search or effective source default?
$effective_source = alpf_get_effective_source( $ns );
$has_params = (
alpf_get_param( 'pf_cat', $ns ) !== null ||
alpf_get_param( 'pf_brand', $ns ) !== null ||
alpf_get_param( 'pf_attr', $ns ) !== null ||
alpf_get_param( 'pf_brand_tax', $ns ) !== null ||
alpf_get_param( 's', $ns ) !== null ||
alpf_get_param( 'min_price', $ns ) !== null ||
alpf_get_param( 'max_price', $ns ) !== null ||
$effective_source !== ''
);
if ( ! $has_params ) return;
$pt = $query->get('post_type');
if ( $pt && $pt !== 'product' && ( ! is_array($pt) || ! in_array( 'product', (array) $pt, true ) ) ) return;
$parts = al_pf_build_tax_query_parts( '', $ns );
if ( $parts ) {
$tax_query = (array) $query->get( 'tax_query' );
$tax_query = array_merge( $tax_query, $parts );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
$query->set( 'tax_query', $tax_query );
}
$s_raw = alpf_get_param( 's', $ns );
if ( $s_raw !== null && $s_raw !== '' ) {
$query->set( 'post_type', array( 'product' ) );
$query->set( 's', sanitize_text_field( wp_unslash( $s_raw ) ) );
}
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( $s_raw !== null && $s_raw !== '', $ns );
$tax_query = array_merge( (array) $query->get('tax_query'), $guard_tax );
$meta_query = array_merge( (array) $query->get('meta_query'), $guard_meta );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query = array_merge( array( 'relation' => 'AND' ), $meta_query );
}
// Apply effective source (e.g., sale) for this namespace, intersect with existing post__in
$args = al_pf_apply_source_to_args( array(), $ns );
if ( ! empty( $args['post__in'] ) ) {
$existing = (array) $query->get( 'post__in' );
if ( ! empty( $existing ) ) {
$query->set( 'post__in', array_values( array_intersect( array_map( 'intval', $existing ), array_map( 'intval', $args['post__in'] ) ) ) );
} else {
$query->set( 'post__in', $args['post__in'] );
}
}
$query->set( 'tax_query', $tax_query );
$query->set( 'meta_query', $meta_query );
}
}
/** Woo 'orderby' map */
if ( ! function_exists( 'al_pf_apply_elementor_orderby_map' ) ) {
function al_pf_apply_elementor_orderby_map( WP_Query $query, $orderby ) {
switch ( $orderby ) {
case 'price':
$query->set( 'meta_key', '_price' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'ASC' ); break;
case 'price-desc':
$query->set( 'meta_key', '_price' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'popularity':
$query->set( 'meta_key', 'total_sales' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'rating':
$query->set( 'meta_key', '_wc_average_rating' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'date':
$query->set( 'orderby', 'date' ); $query->set( 'order', 'DESC' ); break;
case 'menu_order':
case 'default':
default:
$query->set( 'orderby', 'menu_order title' ); $query->set( 'order', 'ASC' ); break;
}
}
}
/** =========================
* AJAX: terms (namespaced)
* ========================= */
if ( ! function_exists( 'al_pf_ajax_terms' ) ) {
function al_pf_ajax_terms() {
if ( empty( $_REQUEST['security'] ) || ! wp_verify_nonce( $_REQUEST['security'], 'alpf_terms' ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ), 403 );
}
$ns = ! empty( $_REQUEST['ns'] ) ? sanitize_key( wp_unslash( $_REQUEST['ns'] ) ) : '';
$brand_tax = '';
$brand_tax_req = alpf_get_param( 'pf_brand_tax', $ns );
if ( ! empty( $brand_tax_req ) ) {
$brand_tax = sanitize_key( wp_unslash( $brand_tax_req ) );
if ( ! taxonomy_exists( $brand_tax ) ) $brand_tax = '';
}
if ( ! $brand_tax ) $brand_tax = al_pf_get_brand_taxonomy();
$attr_tax_slugs = array();
if ( ! empty( $_REQUEST['expose_attrs'] ) ) {
foreach ( explode( ',', (string) wp_unslash( $_REQUEST['expose_attrs'] ) ) as $maybe ) {
$tax = sanitize_key( trim( $maybe ) );
if ( $tax && taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
} else if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
foreach ( wc_get_attribute_taxonomies() as $ga ) {
$tax = 'pa_' . $ga->attribute_name;
if ( taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
$ctx_tax = ! empty( $_REQUEST['ctx_tax'] ) ? sanitize_key( wp_unslash( $_REQUEST['ctx_tax'] ) ) : '';
$ctx_terms = array();
if ( ! empty( $_REQUEST['ctx_terms'] ) ) {
$ctx_terms = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) wp_unslash( $_REQUEST['ctx_terms'] ) ) ) ) );
}
$terms = al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, array(
'tax' => $ctx_tax,
'terms' => $ctx_terms,
), $ns );
$out = array(
'cats' => array(),
'brands' => array(),
'attrs' => array(),
'attr_labels' => array(),
'brand_tax' => $brand_tax,
);
foreach ( (array) $terms['cats'] as $t ) {
$out['cats'][] = array( 'slug'=>$t->slug, 'name'=>$t->name );
}
foreach ( (array) $terms['brands'] as $b ) {
$out['brands'][] = array(
'slug' => $b->slug,
'name' => $b->name,
'logo' => al_pf_get_term_logo_url( $b->term_id ),
);
}
foreach ( (array) $terms['attrs'] as $tax => $arr ) {
$group = array();
foreach ( (array) $arr as $term ) {
$group[] = array( 'slug'=>$term->slug, 'name'=>$term->name, 'tax'=>$tax );
}
$out['attrs'][ $tax ] = $group;
}
$all_attr_taxes = $attr_tax_slugs ? $attr_tax_slugs : array_keys( (array) $terms['attrs'] );
foreach ( $all_attr_taxes as $tax ) {
$out['attr_labels'][ $tax ] = function_exists('wc_attribute_label') ? wc_attribute_label( $tax ) : $tax;
}
nocache_headers();
wp_send_json_success( $out );
}
}
add_action( 'wp_ajax_nopriv_alpf_terms', 'al_pf_ajax_terms' );
add_action( 'wp_ajax_alpf_terms', 'al_pf_ajax_terms' );
/** =========================
* Plugin defaults / developer hooks
* =========================
* Ensure first-paint sale filtering for the common namespace "sale_query_id".
*/
add_filter( 'alpf_default_source_map', function( $map ){
if ( ! is_array( $map ) ) $map = array();
if ( empty( $map['sale_query_id'] ) ) {
$map['sale_query_id'] = 'sale';
}
return $map;
}, 5 );
/** Cache key extra — language/currency awareness (optional but helpful) */
add_filter( 'alpf_transient_key_extra', function( $extra ) {
$lang = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : ( function_exists( 'pll_current_language' ) ? pll_current_language() : '' );
$currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '';
return trim( $extra . '|' . $lang . '|' . $currency, '|' );
}, 5 );
/** Index + bust term scan caches when catalog changes */
if ( ! function_exists( 'alpf_index_transient' ) ) {
function alpf_index_transient( $key ) {
$idx = get_option( 'alpf_terms_keys', array() );
if ( ! in_array( $key, $idx, true ) ) {
$idx[] = $key;
update_option( 'alpf_terms_keys', $idx, false );
}
}
}
if ( ! function_exists( 'alpf_clear_terms_cache' ) ) {
function alpf_clear_terms_cache() {
$idx = (array) get_option( 'alpf_terms_keys', array() );
foreach ( $idx as $k ) { delete_transient( $k ); }
update_option( 'alpf_terms_keys', array(), false );
delete_transient( 'alpf_sale_ids_v1' );
}
}
add_action( 'save_post_product', 'alpf_clear_terms_cache' );
add_action( 'created_term', 'alpf_clear_terms_cache', 10, 3 );
add_action( 'edited_term', 'alpf_clear_terms_cache', 10, 3 );
add_action( 'delete_term', 'alpf_clear_terms_cache', 10, 4 );
/**
* Plugin Name: WooCommerce Qty Plus/Minus Span Controls
* Description: Replaces the default qty input arrows with – [qty] + spans on product and cart pages. Adds a shortcode to place a synced quantity control anywhere on single product pages. Automatically updates the cart after quantity change.
* Version: 1.4.2
* Author: Your Name
* License: GPL2
*/
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Enqueue CSS & JS for qty controls on product and cart pages.
*/
add_action( 'wp_enqueue_scripts', 'wc_qty_span_controls_enqueue_assets' );
function wc_qty_span_controls_enqueue_assets() {
if ( ! is_product() && ! is_cart() ) return;
// Register empty handles so we can attach inline styles/scripts
wp_register_style( 'wc-qty-span-style', false );
wp_enqueue_style( 'wc-qty-span-style' );
wp_register_script( 'wc-qty-span-script', false, array( 'jquery' ), '1.4.2', true );
wp_enqueue_script( 'wc-qty-span-script' );
// Custom CSS for styling the quantity controls
$css = <<<CSS
/* Remove spin buttons for both */
CSS;
wp_add_inline_style( 'wc-qty-span-style', $css );
// Custom JS for quantity plus/minus, syncing, and auto cart update
$js = <<<JS
jQuery(function($){
// Handle clicks on our spans
$(document).on('click', '.qty-btn', function(){
var \$btn = $(this),
\$wrap = \$btn.closest('.quantity'),
\$input = \$wrap.find('input.qty').first(),
step = parseFloat( \$input.attr('step') ) || 1,
min = parseFloat( \$input.attr('min') ) || 0,
max = parseFloat( \$input.attr('max') ),
val = parseFloat( \$input.val() ) || 0;
if ( \$btn.hasClass('qty-inc') ) {
val += step;
if ( ! isNaN(max) && val > max ) val = max;
} else {
val -= step;
if ( val < min ) val = min;
}
\$input.val(val).trigger('change');
});
// Keep all shortcode proxies in sync with the main single-product input
function getMainQtyInput() {
return $('form.cart').first().find('input.qty[name="quantity"]').first();
}
function syncProxiesFromMain() {
var \$main = getMainQtyInput();
if (!\$main.length) return;
var val = \$main.val();
$('.wc-qty-proxy').each(function(){
var \$proxyInput = $(this).find('input.qty').first();
if (\$proxyInput.length && \$proxyInput.val() !== val) {
\$proxyInput.val(val);
}
});
}
function syncMainFromProxy(\$proxyInput) {
var \$main = getMainQtyInput();
if (!\$main.length) return;
var val = \$proxyInput.val();
if (\$main.val() !== val) {
\$main.val(val).trigger('change');
}
}
// On proxy change, update main
$(document).on('change keyup input', '.wc-qty-proxy input.qty', function(){
syncMainFromProxy($(this));
});
// When main changes (e.g., by native control or other scripts), update proxies
$(document).on('change keyup updated_wc_qty', 'form.cart input.qty[name="quantity"]', function(){
syncProxiesFromMain();
});
// Initial sync on load (handles variations initializing qty later as well)
var initSync = function(){ syncProxiesFromMain(); };
$(document).on('found_variation reset_data', initSync);
$(document).ready(initSync);
// On cart, auto-submit when qty changes (for classic and Elementor carts)
$(document).on('change', 'table.cart input.qty', function(){
var \$form = $(this).closest('form.woocommerce-cart-form');
var \$updateBtn = \$form.find('button[name="update_cart"]');
if (\$updateBtn.length) {
\$updateBtn.prop('disabled', false);
\$updateBtn.trigger('click');
}
});
});
JS;
wp_add_inline_script( 'wc-qty-span-script', $js );
}
/**
* Output the “−” span before the qty input.
*/
add_action( 'woocommerce_before_quantity_input_field', 'wc_qty_span_add_dec' );
function wc_qty_span_add_dec() {
echo '<span class="qty-btn qty-dec" tabindex="0" aria-label="Decrease quantity">−</span>';
}
/**
* Output the “+” span after the qty input.
*/
add_action( 'woocommerce_after_quantity_input_field', 'wc_qty_span_add_inc' );
function wc_qty_span_add_inc() {
echo '<span class="qty-btn qty-inc" tabindex="0" aria-label="Increase quantity">+</span>';
}
/**
* Shortcode: [wc_qty_control]
* Renders a synced quantity control anywhere on a single product page.
*
* Attributes:
* - product_id (int) Optional; defaults to current product.
* - min, max, step (numbers) Optional; fall back to product purchase constraints.
* - value (number) Optional; initial value (defaults to main input / product min).
* - class (string) Optional; extra CSS class on wrapper.
*
* Notes:
* - Uses woocommerce_quantity_input() so the +/- spans still apply via the hooks above.
* - Uses a proxy input name to avoid altering POST; JS keeps it in sync with main qty.
*/
add_shortcode( 'wc_qty_control', 'wc_qty_span_shortcode_render' );
function wc_qty_span_shortcode_render( $atts ) {
if ( ! is_product() ) return ''; // Only makes sense on single product pages
$atts = shortcode_atts( array(
'product_id' => 0,
'min' => '',
'max' => '',
'step' => '',
'value' => '',
'class' => '',
), $atts, 'wc_qty_control' );
// Resolve product safely without colliding with $product global
$wc_product = null;
if ( absint( $atts['product_id'] ) > 0 ) {
$wc_product = wc_get_product( absint( $atts['product_id'] ) );
}
if ( ! $wc_product instanceof WC_Product ) {
global $product; // from the single product loop
if ( $product instanceof WC_Product ) {
$wc_product = $product;
}
}
if ( ! $wc_product instanceof WC_Product ) {
return ''; // no valid product context
}
// Determine constraints
$min = ( $atts['min'] !== '' ) ? floatval( $atts['min'] ) : $wc_product->get_min_purchase_quantity();
$max = ( $atts['max'] !== '' ) ? floatval( $atts['max'] ) : $wc_product->get_max_purchase_quantity();
$step = ( $atts['step'] !== '' ) ? floatval( $atts['step'] ) : apply_filters( 'woocommerce_quantity_input_step', 1, $wc_product );
// Determine starting value: prefer attribute, else POSTed main input, else product min
$value = ( $atts['value'] !== '' )
? floatval( $atts['value'] )
: ( isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $min );
if ( $value < $min ) $value = $min;
if ( $max && $value > $max ) $value = $max;
// Build args for a proxy input. Name is different so it doesn't interfere with add-to-cart POST.
$args = array(
'input_name' => 'qty_proxy',
'input_value' => $value,
'min_value' => $min,
'max_value' => $max ? $max : '',
'step' => $step,
'classes' => array( 'qty', 'text', 'qty-proxy-input' ),
'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', '[0-9]*' ),
'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', 'numeric' ),
'product_name' => $wc_product->get_name(),
);
// Capture and return the HTML (must echo the returned string)
ob_start();
echo '<div class="wc-qty-proxy ' . esc_attr( $atts['class'] ) . '" data-product-id="' . esc_attr( $wc_product->get_id() ) . '">';
echo woocommerce_quantity_input( $args, $wc_product, false );
echo '</div>';
return ob_get_clean();
}
add_action( 'init', function() {
add_shortcode( 'wc_product_labels', 'wcplqt_product_labels_shortcode' );
} );
/**
* Shortcode.
* [wc_product_labels id="123" sale_label="Sale" outofstock_label="Sold out"]
*/
function wcplqt_product_labels_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'id' => 0,
'sale_label' => '',
'outofstock_label' => '',
),
$atts,
'wc_product_labels'
);
$product = null;
if ( $atts['id'] ) {
$product = wc_get_product( (int) $atts['id'] );
} elseif ( isset( $GLOBALS['product'] ) && is_a( $GLOBALS['product'], 'WC_Product' ) ) {
$product = $GLOBALS['product'];
}
if ( ! $product ) {
return '';
}
$labels = array();
$product_id = $product->get_id();
$sale_tmpl = $atts['sale_label'] !== '' ? $atts['sale_label'] : __( 'Sale', 'woocommerce' );
$oos_tmpl = $atts['outofstock_label'] !== '' ? $atts['outofstock_label'] : __( 'Out of Stock', 'woocommerce' );
$is_oos = ! $product->is_in_stock();
$is_on_sale = $product->is_on_sale();
// Out-of-stock: show ONLY the OOS badge.
if ( $is_oos ) {
$labels[] = '<span class="wc-badge wc-badge--out-of-stock">' . esc_html( $oos_tmpl ) . '</span>';
} else {
// In stock: show Sale badge if applicable.
if ( $is_on_sale ) {
$labels[] = '<span class="wc-badge wc-badge--sale">' . esc_html( $sale_tmpl ) . '</span>';
}
}
$attrs = array(
'class' => 'wc-product-labels',
'data-product-id' => (string) (int) $product_id,
);
$attr_html = '';
foreach ( $attrs as $k => $v ) {
$attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
}
// If no labels, return empty string (prevents an empty wrapper).
if ( empty( $labels ) ) {
return '';
}
$html = '<div' . $attr_html . '>' . implode( "\n", $labels ) . '</div>';
return $html;
}
/**
* CSS.
*/
add_action( 'wp_enqueue_scripts', function() {
$css = "
";
$handle = 'woocommerce-general';
if ( wp_style_is( $handle, 'enqueued' ) || wp_style_is( $handle, 'registered' ) ) {
wp_add_inline_style( $handle, $css );
} else {
add_action( 'wp_head', function() use ( $css ) {
echo '<style id="wcplqt-inline">' . $css . '</style>';
} );
}
}, 20 );
/**
* Plugin Name: AL — Woo Term Thumbnail (Loop-Safe) for Elementor
* Description: Dynamic tag that returns the current loop item's WooCommerce product category image (works in Product Taxonomy Loop Grid).
* Version: 1.0.0
* Author: Your Team
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
add_action( 'elementor/dynamic_tags/register', function( $manager ) {
// Simple image dynamic tag for term thumbnails
class AL_Woo_Term_Thumb_Tag extends \Elementor\Core\DynamicTags\Data_Tag {
public function get_name(): string { return 'al-woo-term-thumb'; }
public function get_title(): string { return esc_html__( 'Woo Term Thumbnail (Loop-Safe)', 'al' ); }
public function get_group(): array { return [ 'site' ]; }
// Tell Elementor this tag returns an IMAGE so it appears on Image widgets
public function get_categories(): array {
return [ \Elementor\Modules\DynamicTags\Module::IMAGE_CATEGORY ];
}
public function get_value( array $options = [] ) {
$term_id = 0;
// Inside Elementor term loop, the current term lives here:
global $wp_query;
if ( isset( $wp_query->loop_term ) && isset( $wp_query->loop_term->term_id ) ) {
$term_id = (int) $wp_query->loop_term->term_id;
}
// Fallback: on a product_cat archive, use the queried term
if ( ! $term_id && is_tax( 'product_cat' ) ) {
$term_id = (int) get_queried_object_id();
}
if ( ! $term_id ) {
return false;
}
// WooCommerce stores category image ID in term meta key 'thumbnail_id'
$attachment_id = (int) get_term_meta( $term_id, 'thumbnail_id', true );
if ( ! $attachment_id ) {
return false;
}
$url = wp_get_attachment_url( $attachment_id );
if ( ! $url ) {
return false;
}
// Elementor expects an array with id/url for IMAGE_CATEGORY tags
return [
'id' => $attachment_id,
'url' => $url,
];
}
}
$manager->register( new AL_Woo_Term_Thumb_Tag() );
} );
/**
* Change "Add to Cart" text on single product pages.
*/
add_filter( 'woocommerce_product_single_add_to_cart_text', 'wc_custom_single_add_to_cart_text', 10, 2 );
function wc_custom_single_add_to_cart_text( $text, $product ) {
// Example: different text based on product type
if ( $product->is_type( 'simple' ) ) {
return __( 'Kosárba', 'your-textdomain' );
}
if ( $product->is_type( 'variable' ) ) {
return __( 'Kosárba', 'your-textdomain' );
}
// Default fallback
return __( 'Add to Basket', 'your-textdomain' );
}
/**
* Plugin Name: Custom Frontend Signup Form (Shortcode)
* Description: Provides [custom_signup_form] shortcode to register users. Customizable field labels, Terms, and logged-in notice via shortcode attributes.
* Version: 1.3.0
* Author: Your Name
* Text Domain: cfsf
*/
if ( ! defined( 'ABSPATH' ) ) exit;
final class CFSF_Custom_Signup {
const NONCE_ACTION = 'cfsf_signup_action';
const NONCE_NAME = 'cfsf_signup_nonce';
const SHORTCODE = 'custom_signup_form';
const RATE_KEY = 'cfsf_rate_';
const RATE_MAX = 5; // submissions per hour
const RATE_WINDOW = 3600; // seconds
public function __construct() {
add_shortcode( self::SHORTCODE, [ $this, 'render_shortcode' ] );
add_action( 'init', [ $this, 'maybe_handle_post' ] );
}
/** Handle form submission early so we can redirect cleanly */
public function maybe_handle_post() {
if ( empty( $_POST['cfsf_submitted'] ) ) {
return;
}
// Nonce
if ( ! isset( $_POST[ self::NONCE_NAME ] ) || ! wp_verify_nonce( $_POST[ self::NONCE_NAME ], self::NONCE_ACTION ) ) {
$this->store_error( __( 'Security check failed. Please refresh and try again.', 'cfsf' ) );
return;
}
// Honeypot
if ( ! empty( $_POST['cfsf_hp'] ) ) {
$this->store_error( __( 'Spam detected.', 'cfsf' ) );
return;
}
// Rate limiting
if ( ! $this->check_rate_limit() ) {
$this->store_error( __( 'Too many attempts. Please try again in a little while.', 'cfsf' ) );
return;
}
// Redirect target
$redirect = isset( $_POST['cfsf_redirect'] ) ? $this->sanitize_redirect( wp_unslash( $_POST['cfsf_redirect'] ) ) : home_url( '/' );
// Logged-in visitors: just go where they asked
if ( is_user_logged_in() ) {
wp_safe_redirect( $redirect );
exit;
}
// Inputs
$first_name = isset( $_POST['first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['first_name'] ) ) : '';
$last_name = isset( $_POST['last_name'] ) ? sanitize_text_field( wp_unslash( $_POST['last_name'] ) ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$password = isset( $_POST['password'] ) ? (string) $_POST['password'] : '';
$phone = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '';
// Flags
$require_phone = isset( $_POST['cfsf_require_phone'] ) && $_POST['cfsf_require_phone'] === 'yes';
$require_terms = isset( $_POST['cfsf_require_terms'] ) && $_POST['cfsf_require_terms'] === 'yes';
// Validate
$errors = new WP_Error();
if ( empty( $first_name ) ) {
$errors->add( 'first_name', __( 'First name is required.', 'cfsf' ) );
}
if ( empty( $last_name ) ) {
$errors->add( 'last_name', __( 'Last name is required.', 'cfsf' ) );
}
if ( empty( $email ) || ! is_email( $email ) ) {
$errors->add( 'email', __( 'A valid email is required.', 'cfsf' ) );
} elseif ( email_exists( $email ) ) {
$errors->add( 'email_exists', __( 'An account with this email already exists.', 'cfsf' ) );
}
if ( empty( $password ) || strlen( $password ) < 8 ) {
$errors->add( 'password', __( 'Password must be at least 8 characters.', 'cfsf' ) );
}
if ( $require_phone && empty( $phone ) ) {
$errors->add( 'phone', __( 'Phone number is required.', 'cfsf' ) );
}
if ( $require_terms && empty( $_POST['terms_agree'] ) ) {
$errors->add( 'terms', __( 'You must agree to the Terms to continue.', 'cfsf' ) );
}
if ( $errors->has_errors() ) {
$this->store_error( $errors );
return;
}
/** Hook before user creation */
do_action( 'cfsf_before_create_user', $_POST );
// Create user (email as login)
$user_id = wp_create_user( $email, $password, $email );
if ( is_wp_error( $user_id ) ) {
$code = $user_id->get_error_code();
if ( 'existing_user_login' === $code ) {
$this->store_error( __( 'A user with that login already exists.', 'cfsf' ) );
} elseif ( 'existing_user_email' === $code ) {
$this->store_error( __( 'An account with this email already exists.', 'cfsf' ) );
} else {
$this->store_error( $user_id );
}
return;
}
// Profile basics
wp_update_user( [
'ID' => $user_id,
'first_name' => $first_name,
'last_name' => $last_name,
'role' => get_option( 'default_role', 'subscriber' ),
] );
// Phone meta (generic only)
if ( ! empty( $phone ) ) {
update_user_meta( $user_id, 'phone', $phone );
}
/** Hook after user creation */
do_action( 'cfsf_after_create_user', $user_id, $_POST );
// Auto-login & redirect
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id, true );
do_action( 'wp_login', get_userdata( $user_id )->user_login, get_userdata( $user_id ) );
wp_safe_redirect( $redirect ? $redirect : home_url( '/' ) );
exit;
}
/** Render the signup form */
public function render_shortcode( $atts ) {
// Normalize keys to avoid builder case quirks
$atts = array_change_key_case( (array) $atts, CASE_LOWER );
$defaults = [
// Behavior
'require_phone' => 'no',
'require_terms' => 'no',
'redirect' => '',
// Labels (plain text)
'label_first_name' => __( 'First Name', 'cfsf' ),
'label_last_name' => __( 'Last Name', 'cfsf' ),
'label_email' => __( 'Email', 'cfsf' ),
'label_password' => __( 'Password', 'cfsf' ),
'label_phone' => __( 'Phone', 'cfsf' ),
'label_submit' => __( 'Create Account', 'cfsf' ),
// Terms: prefer label_terms_html if provided; otherwise build from url/text
'label_terms_html' => '',
'terms_url' => '/terms',
'terms_text' => __( 'Terms & Conditions', 'cfsf' ),
// Logged-in notice (NEW)
'logged_in_message' => __( 'You are already logged in.', 'cfsf' ),
'logged_in_link_text' => __( 'Go to my account', 'cfsf' ),
'logged_in_link_url' => '',
// Legacy (ignored gracefully)
'include_woo' => '',
];
$atts = shortcode_atts( $defaults, $atts, self::SHORTCODE );
// Flags
$require_phone = ( strtolower( $atts['require_phone'] ) === 'yes' );
$require_terms = ( strtolower( $atts['require_terms'] ) === 'yes' );
// Redirect (allow relative URLs)
$redirect = ! empty( $atts['redirect'] ) ? esc_url( $this->sanitize_redirect( $atts['redirect'] ) ) : esc_url( home_url( '/' ) );
// Helper: decode any builder-escaped values
$decode = static function( $val ) {
return trim( wp_specialchars_decode( (string) $val, ENT_QUOTES ) );
};
// Labels (decoded; we escape on output)
$label_first_name = $decode( $atts['label_first_name'] );
$label_last_name = $decode( $atts['label_last_name'] );
$label_email = $decode( $atts['label_email'] );
$label_password = $decode( $atts['label_password'] );
$label_phone = $decode( $atts['label_phone'] );
$label_submit = $decode( $atts['label_submit'] );
// Terms label (HTML allowed)
$label_terms_html = $decode( $atts['label_terms_html'] );
if ( $label_terms_html === '' ) {
$terms_url = esc_url( $decode( $atts['terms_url'] ) );
$terms_text = esc_html( $decode( $atts['terms_text'] ) );
$label_terms_html = sprintf(
'I agree to the <a href="%s" target="_blank" rel="noopener">%s</a>',
$terms_url ?: esc_url( home_url( '/terms' ) ),
$terms_text ?: esc_html__( 'Terms & Conditions', 'cfsf' )
);
}
// Logged-in text (NEW)
$logged_in_message = $decode( $atts['logged_in_message'] );
$logged_in_link_text = $decode( $atts['logged_in_link_text'] );
// Default account URL: Woo My Account → WP profile → homepage
$default_account_url = function_exists( 'wc_get_page_permalink' )
? wc_get_page_permalink( 'myaccount' )
: admin_url( 'profile.php' );
if ( empty( $default_account_url ) ) {
$default_account_url = home_url( '/' );
}
$logged_in_link_url = $atts['logged_in_link_url'] !== ''
? esc_url( $this->sanitize_redirect( $decode( $atts['logged_in_link_url'] ) ) )
: esc_url( $default_account_url );
ob_start();
// If logged in, show customizable notice instead of the form
if ( is_user_logged_in() ) : ?>
<div class="cfsf-success">
<?php echo esc_html( $logged_in_message ); ?>
<?php if ( $logged_in_link_url ) : ?>
<a href="<?php echo esc_url( $logged_in_link_url ); ?>"><?php echo esc_html( $logged_in_link_text ); ?></a>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
endif;
// Show stored errors
$this->output_errors();
?>
<form method="post" class="cfsf-signup-form" action="">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); ?>
<input type="hidden" name="cfsf_submitted" value="1" />
<input type="text" name="cfsf_hp" value="" style="display:none!important;" tabindex="-1" autocomplete="off" aria-hidden="true" />
<input type="hidden" name="cfsf_require_phone" value="<?php echo $require_phone ? 'yes' : 'no'; ?>" />
<input type="hidden" name="cfsf_require_terms" value="<?php echo $require_terms ? 'yes' : 'no'; ?>" />
<input type="hidden" name="cfsf_redirect" value="<?php echo esc_url( $redirect ); ?>" />
<div class="cfsf-row">
<label for="first_name"><?php echo esc_html( $label_first_name ); ?> *</label>
<input type="text" id="first_name" name="first_name" required autocomplete="given-name" />
</div>
<div class="cfsf-row">
<label for="last_name"><?php echo esc_html( $label_last_name ); ?> *</label>
<input type="text" id="last_name" name="last_name" required autocomplete="family-name" />
</div>
<div class="cfsf-row">
<label for="email"><?php echo esc_html( $label_email ); ?> *</label>
<input type="email" id="email" name="email" required autocomplete="email" />
</div>
<div class="cfsf-row">
<label for="password"><?php echo esc_html( $label_password ); ?> *</label>
<input type="password" id="password" name="password" minlength="8" required autocomplete="new-password" />
</div>
<div class="cfsf-row">
<label for="phone"><?php echo esc_html( $label_phone ); ?><?php echo $require_phone ? ' *' : ''; ?></label>
<input type="tel" id="phone" name="phone" <?php echo $require_phone ? 'required' : ''; ?> autocomplete="tel" />
</div>
<?php if ( $require_terms ) : ?>
<div class="cfsf-row cfsf-terms">
<label class="cfsf-terms-label">
<input type="checkbox" name="terms_agree" value="1" />
<?php
echo wp_kses(
$label_terms_html,
[
'a' => [ 'href' => [], 'target' => [], 'rel' => [] ],
'strong' => [],
'em' => [],
'span' => [ 'class' => [] ],
]
);
?>
</label>
</div>
<?php endif; ?>
<div class="cfsf-row">
<button type="submit"><?php echo esc_html( $label_submit ); ?></button>
</div>
</form>
<style>
</style>
<?php
return ob_get_clean();
}
/** Store errors to transient for post-redirect display */
private function store_error( $error ) {
$msgs = [];
if ( is_wp_error( $error ) ) {
$msgs = $error->get_error_messages();
} else {
$msgs = [ (string) $error ];
}
set_transient( 'cfsf_errors_' . $this->session_key(), $msgs, MINUTE_IN_SECONDS * 5 );
}
/** Output & clear stored errors */
private function output_errors() {
$key = 'cfsf_errors_' . $this->session_key();
$errors = get_transient( $key );
if ( $errors && is_array( $errors ) && ! empty( $errors ) ) {
echo '<div class="cfsf-errors"><strong>' . esc_html__( 'Please fix the following:', 'cfsf' ) . '</strong><ul>';
foreach ( $errors as $msg ) {
echo '<li>' . esc_html( $msg ) . '</li>';
}
echo '</ul></div>';
delete_transient( $key );
}
}
/** Lightweight per-visitor session key without PHP sessions */
private function session_key() {
if ( ! isset( $_COOKIE['cfsf_key'] ) ) {
$val = wp_generate_password( 20, false );
setcookie( 'cfsf_key', $val, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
$_COOKIE['cfsf_key'] = $val;
}
return sanitize_text_field( $_COOKIE['cfsf_key'] );
}
private function client_ip() {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
return sanitize_text_field( $ip );
}
private function check_rate_limit() : bool {
$ip = $this->client_ip();
$key = self::RATE_KEY . md5( $ip );
$data = get_transient( $key );
if ( empty( $data ) || ! is_array( $data ) ) {
set_transient( $key, [ 'count' => 1, 'start' => time() ], self::RATE_WINDOW );
return true;
}
$count = isset( $data['count'] ) ? (int) $data['count'] : 0;
$start = isset( $data['start'] ) ? (int) $data['start'] : time();
if ( ( time() - $start ) > self::RATE_WINDOW ) {
set_transient( $key, [ 'count' => 1, 'start' => time() ], self::RATE_WINDOW );
return true;
}
if ( $count >= self::RATE_MAX ) {
return false;
}
$data['count'] = $count + 1;
set_transient( $key, $data, self::RATE_WINDOW - ( time() - $start ) );
return true;
}
/** Only allow same-host redirects (extend with your own whitelist if needed). */
private function sanitize_redirect( $url ) {
$url = esc_url_raw( $url );
if ( empty( $url ) ) {
return home_url( '/' );
}
$host_allowed = parse_url( home_url(), PHP_URL_HOST );
$host_target = parse_url( $url, PHP_URL_HOST );
if ( $host_target && ! hash_equals( $host_allowed, $host_target ) ) {
return home_url( '/' );
}
return $url;
}
}
new CFSF_Custom_Signup();
// Add "Quantity" label above the quantity input field on single product page
add_action( 'woocommerce_before_add_to_cart_quantity', 'custom_quantity_label' );
function custom_quantity_label() {
echo '<label for="quantity" class="quantity-label" style="display:block;margin-bottom:5px;font-weight:600;">' . __( 'Quantity', 'woocommerce' ) . '</label>';
}
/* Display an order bump before the order review section on the WooCommerce checkout page. */
function aovup_custom_order_bump() {
// Check if it's the checkout page
echo '<div class="custom-order-bump">';
echo do_shortcode('[cuw_checkout_upsells]');
echo '</div>';
}
add_action('woocommerce_checkout_before_order_review', 'aovup_custom_order_bump');
/**
* Plugin Name: AL — Woo + Elementor Product Filter Bar (Multi-Grid Scoped + AJAX Rehydrate + Modern Preloader)
* Description: Filter bar for Woo/Elementor Loop Grid. Per-instance namespacing so multiple filter bars & grids on the same page work independently. Archive-aware, preserves params, rehydrates Elementor, accessible UI. Includes optional "source" (e.g. sale) and per-QueryID default source mapping for first-paint correctness. Brand logos, empty-state messages, and performance/a11y improvements included.
* Version: 1.8.4
* Author: Art & Living
* Text Domain: al-woo-ele-filter
*/
if ( ! defined( 'ABSPATH' ) ) exit;
/** =========================
* Dynamic Elementor Query ID registry
* ========================= */
if ( ! function_exists( 'al_pf_register_elementor_query_id' ) ) {
function al_pf_register_elementor_query_id( $ids ) {
static $registered = array();
$ids = is_array( $ids ) ? $ids : array( $ids );
foreach ( $ids as $raw ) {
$id = sanitize_key( $raw );
if ( ! $id || isset( $registered[ $id ] ) ) continue;
add_action( "elementor/query/{$id}", 'al_pf_elementor_posts_query', 5 );
add_action( "elementor_pro/posts/query/{$id}", 'al_pf_elementor_posts_query', 5 );
$registered[ $id ] = true;
}
}
}
// Defaults for compatibility (+ pre-register sale_query_id to ensure early hook availability)
al_pf_register_elementor_query_id( array( 'archive_loop_grid', 'alpf', 'sale_query_id' ) );
/** =========================
* Param helpers (namespaced)
* ========================= */
if ( ! function_exists( 'alpf_get_param' ) ) {
function alpf_get_param( $key, $ns = '' ) {
$key = (string) $key;
$ns = $ns ? sanitize_key( $ns ) : '';
if ( $ns ) {
$nk = "{$ns}_{$key}";
if ( isset( $_GET[ $nk ] ) && $_GET[ $nk ] !== '' ) {
return $_GET[ $nk ];
}
}
return isset( $_GET[ $key ] ) ? $_GET[ $key ] : null;
}
}
if ( ! function_exists( 'alpf_get_param_list' ) ) {
function alpf_get_param_list( $key, $ns = '' ) {
$val = alpf_get_param( $key, $ns );
if ( $val === null || $val === '' ) return array();
$arr = array_map( 'trim', explode( ',', (string) wp_unslash( $val ) ) );
return array_values( array_filter( $arr, 'strlen' ) );
}
}
/** =========================
* Helpers
* ========================= */
if ( ! function_exists( 'al_pf_get_brand_taxonomy' ) ) {
function al_pf_get_brand_taxonomy( $preferred = '' ) {
$preferred = $preferred ? sanitize_key( $preferred ) : '';
if ( $preferred && taxonomy_exists( $preferred ) ) return $preferred;
if ( taxonomy_exists( 'product_brand' ) ) return 'product_brand';
if ( taxonomy_exists( 'yith_product_brand' ) ) return 'yith_product_brand';
if ( taxonomy_exists( 'pa_brand' ) ) return 'pa_brand';
return '';
}
}
/** Detect namespaced/un-namespaced PF params anywhere in query string */
if ( ! function_exists( 'alpf_request_has_pf_params_globally' ) ) {
function alpf_request_has_pf_params_globally() {
foreach ( (array) $_GET as $k => $v ) {
if ( ! is_string( $k ) ) continue;
if ( preg_match( '/(?:^|_)pf_(?:cat|brand|attr|brand_tax|source|min_price|max_price)\z/', $k ) ) {
if ( $v !== '' && $v !== null ) return true;
}
}
return false;
}
}
/** =========================
* Brand/logo helpers
* ========================= */
if ( ! function_exists( 'al_pf_get_term_logo_url' ) ) {
/**
* Resolve a brand term's logo URL using common meta keys.
* Filters:
* - alpf_brand_logo_meta_keys : array of possible meta keys for logo attachment id
* - alpf_brand_logo_url : override the resolved URL
*/
function al_pf_get_term_logo_url( $term_id ) {
$keys = apply_filters( 'alpf_brand_logo_meta_keys', array(
'thumbnail_id',
'brand_thumbnail_id',
'brand_logo_id',
'logo_id',
) );
$url = '';
foreach ( (array) $keys as $meta_key ) {
$att_id = absint( get_term_meta( $term_id, $meta_key, true ) );
if ( $att_id ) {
$src = wp_get_attachment_image_src( $att_id, 'thumbnail' );
if ( $src && ! empty( $src[0] ) ) { $url = $src[0]; break; }
}
}
return (string) apply_filters( 'alpf_brand_logo_url', $url, $term_id );
}
}
/** =========================
* Source handling (sale / all) — URL param or default map
* ========================= */
if ( ! function_exists( 'alpf_get_effective_source' ) ) {
function alpf_get_effective_source( $ns = '' ) {
$ns = $ns ? sanitize_key( $ns ) : '';
$src = alpf_get_param( 'pf_source', $ns );
if ( $src !== null && $src !== '' ) {
return sanitize_key( wp_unslash( $src ) );
}
$map = apply_filters( 'alpf_default_source_map', array() );
if ( is_array( $map ) ) {
if ( $ns && ! empty( $map[ $ns ] ) ) return sanitize_key( $map[ $ns ] );
if ( isset( $map[''] ) && $map[''] ) return sanitize_key( $map[''] );
}
return '';
}
}
if ( ! function_exists( 'al_pf_get_sale_ids' ) ) {
function al_pf_get_sale_ids() {
// Cached for performance on large catalogs
$cache_key = 'alpf_sale_ids_v1';
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) return $cached;
$ids = function_exists( 'wc_get_product_ids_on_sale' ) ? wc_get_product_ids_on_sale() : array();
$ids = is_array( $ids ) ? array_map( 'intval', $ids ) : array();
$ttl = (int) apply_filters( 'alpf_sale_ids_ttl', 5 * MINUTE_IN_SECONDS );
set_transient( $cache_key, $ids, $ttl );
return $ids;
}
}
if ( ! function_exists( 'al_pf_apply_source_to_args' ) ) {
function al_pf_apply_source_to_args( $args, $ns = '' ) {
$src = alpf_get_effective_source( $ns );
if ( $src === 'sale' ) {
$sale_ids = al_pf_get_sale_ids();
if ( ! empty( $sale_ids ) ) {
$args['post__in'] = $sale_ids;
}
}
return $args;
}
}
/** =========================
* Query parts
* ========================= */
if ( ! function_exists( 'al_pf_build_tax_query_parts' ) ) {
function al_pf_build_tax_query_parts( $brand_tax = '', $ns = '' ) {
$parts = array();
$cats_raw = alpf_get_param_list( 'pf_cat', $ns );
if ( $cats_raw ) {
$cats = array_map( 'sanitize_title', $cats_raw );
if ( $cats ) $parts[] = array( 'taxonomy'=>'product_cat', 'field'=>'slug', 'terms'=>$cats, 'operator'=>'IN' );
}
if ( ! $brand_tax ) {
$brand_tax_raw = alpf_get_param( 'pf_brand_tax', $ns );
if ( ! empty( $brand_tax_raw ) ) {
$brand_tax = sanitize_key( wp_unslash( $brand_tax_raw ) );
if ( ! taxonomy_exists( $brand_tax ) ) $brand_tax = '';
}
if ( ! $brand_tax ) $brand_tax = al_pf_get_brand_taxonomy();
}
$brands_raw = alpf_get_param_list( 'pf_brand', $ns );
if ( $brand_tax && $brands_raw ) {
$brands = array_map( 'sanitize_title', $brands_raw );
if ( $brands ) $parts[] = array( 'taxonomy'=>$brand_tax, 'field'=>'slug', 'terms'=>$brands, 'operator'=>'IN' );
}
$attr_raw = alpf_get_param_list( 'pf_attr', $ns );
if ( $attr_raw ) {
$attr_terms_by_tax = array();
foreach ( $attr_raw as $p ) {
if ( strpos( $p, ':' ) === false ) continue;
list( $tax, $slug ) = array_map( 'sanitize_key', explode( ':', $p, 2 ) );
if ( $tax && $slug && taxonomy_exists( $tax ) ) $attr_terms_by_tax[ $tax ][] = $slug;
}
foreach ( $attr_terms_by_tax as $tax => $slugs ) {
$parts[] = array(
'taxonomy' => $tax,
'field' => 'slug',
'terms' => array_values( array_unique( $slugs ) ),
'operator' => 'IN',
);
}
}
return $parts;
}
}
if ( ! function_exists( 'alpf_build_catalog_guards' ) ) {
function alpf_build_catalog_guards( $is_search = false, $ns = '' ) {
$tax_query = array();
$meta_query = array();
$vis_terms = array( 'exclude-from-catalog' );
if ( $is_search ) $vis_terms[] = 'exclude-from-search';
$tax_query[] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => $vis_terms,
'operator' => 'NOT IN',
);
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$meta_query[] = array(
'key' => '_stock_status',
'value' => 'outofstock',
'compare' => '!=',
);
$tax_query[] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => array( 'outofstock' ),
'operator' => 'NOT IN',
);
}
// Safer price bounds
$min_price_raw = alpf_get_param( 'min_price', $ns );
$max_price_raw = alpf_get_param( 'max_price', $ns );
$has_min = ($min_price_raw !== null && $min_price_raw !== '');
$has_max = ($max_price_raw !== null && $max_price_raw !== '');
if ( $has_min || $has_max ) {
$min = $has_min ? max( 0, floatval( wp_unslash( $min_price_raw ) ) ) : 0;
$max = $has_max ? max( $min, floatval( wp_unslash( $max_price_raw ) ) ) : $min;
$meta_query[] = array(
'key' => '_price',
'type' => 'DECIMAL',
'compare' => 'BETWEEN',
'value' => array( (string) $min, (string) $max ),
);
}
return array( $tax_query, $meta_query );
}
}
/** =========================
* Collect terms for UI (respects effective source)
* ========================= */
if ( ! function_exists( 'al_pf_collect_terms_for_filters' ) ) {
function al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, $context = array(), $ns = '' ) {
$s_query = alpf_get_param( 's', $ns );
$s_query = $s_query ? sanitize_text_field( wp_unslash( $s_query ) ) : '';
$pf_cat_s = alpf_get_param( 'pf_cat', $ns );
$pf_brand_s = alpf_get_param( 'pf_brand', $ns );
$pf_attr_s = alpf_get_param( 'pf_attr', $ns );
$ctx_tax = '';
$ctx_terms = array();
if ( empty( $context ) ) {
if ( function_exists( 'is_tax' ) && is_tax() ) {
$qo = get_queried_object();
if ( $qo && ! empty( $qo->taxonomy ) && taxonomy_exists( $qo->taxonomy ) && is_object_in_taxonomy( 'product', $qo->taxonomy ) ) {
$ctx_tax = $qo->taxonomy;
$ctx_terms = array( $qo->slug );
}
}
} else {
$ctx_tax = ! empty( $context['tax'] ) ? sanitize_key( $context['tax'] ) : '';
$ctx_terms = ! empty( $context['terms'] ) ? array_filter( array_map( 'sanitize_title', (array) $context['terms'] ) ) : array();
}
$pf_cats = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $pf_cat_s ) ) ) );
$pf_brands = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $pf_brand_s ) ) ) );
$attr_terms_by_tax = array();
if ( $pf_attr_s ) {
foreach ( array_map( 'trim', explode( ',', (string) $pf_attr_s ) ) as $p ) {
if ( strpos( $p, ':' ) === false ) continue;
list( $tax, $slug ) = array_map( 'sanitize_key', explode( ':', $p, 2 ) );
if ( $tax && $slug && taxonomy_exists( $tax ) ) $attr_terms_by_tax[ $tax ][] = $slug;
}
}
$tax_query = array();
if ( $ctx_tax && $ctx_terms ) {
$tax_query[] = array(
'taxonomy' => $ctx_tax,
'field' => 'slug',
'terms' => $ctx_terms,
'operator' => 'IN',
);
}
if ( $pf_cats ) $tax_query[] = array( 'taxonomy'=>'product_cat', 'field'=>'slug', 'terms'=>$pf_cats, 'operator'=>'IN' );
if ( $brand_tax && $pf_brands ) $tax_query[] = array( 'taxonomy'=>$brand_tax, 'field'=>'slug', 'terms'=>$pf_brands, 'operator'=>'IN' );
if ( $attr_terms_by_tax ) {
foreach ( $attr_terms_by_tax as $tax => $slugs ) {
$tax_query[] = array(
'taxonomy' => $tax,
'field'=>'slug',
'terms' => array_values( array_unique( $slugs ) ),
'operator' => 'IN',
);
}
}
if ( count( $tax_query ) > 1 ) $tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( ! empty( $s_query ), $ns );
$extra_key_bits = (string) apply_filters( 'alpf_transient_key_extra', '' );
$cache_key = 'al_pf_terms_' . md5( wp_json_encode( array(
'ns' => $ns,
's' => $s_query,
'cats' => $pf_cats,
'brand_tax' => $brand_tax,
'brands' => $pf_brands,
'attrs' => $attr_terms_by_tax,
'ctx' => array( 'tax' => $ctx_tax, 'terms' => $ctx_terms ),
'expose' => array_values( array_unique( (array) $attr_tax_slugs ) ),
'source' => (string) alpf_get_effective_source($ns),
'ver' => 'v24-mobile-no-overlay-gap'
) ) . '|' . $extra_key_bits );
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) return $cached;
$final_tax = array_merge( (array) $tax_query, (array) $guard_tax );
if ( count( $final_tax ) > 1 && ! isset( $final_tax['relation'] ) ) {
$final_tax = array_merge( array( 'relation' => 'AND' ), $final_tax );
}
// Optional SKU search (off by default)
$include_sku = (bool) apply_filters( 'alpf_scan_include_sku', false );
$scan_meta = $guard_meta;
if ( $s_query && $include_sku ) {
$scan_meta = array_merge(
array( 'relation' => 'AND' ),
(array) $guard_meta,
array(
array(
'relation' => 'OR',
array(
'key' => '_sku',
'value' => $s_query,
'compare' => 'LIKE',
),
)
)
);
}
$scan_args = array(
'post_type' => 'product',
'post_status' => 'publish',
's' => $s_query,
'fields' => 'ids',
'posts_per_page' => (int) apply_filters( 'al_pf_terms_scan_posts_cap', 5000 ),
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'cache_results' => false,
'orderby' => 'none',
'tax_query' => $final_tax,
'meta_query' => $scan_meta,
);
// Apply "effective source" (e.g., sale) for term scan
$scan_args = al_pf_apply_source_to_args( $scan_args, $ns );
$scan = new WP_Query( $scan_args );
$ids = ! is_wp_error( $scan ) ? array_map( 'intval', $scan->posts ) : array();
$max_ids = (int) apply_filters( 'al_pf_terms_scan_cap', 5000 );
if ( count( $ids ) > $max_ids ) {
$out = array( 'cats'=>array(), 'brands'=>array(), 'attrs'=>array() );
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
$out = array( 'cats' => array(), 'brands' => array(), 'attrs' => array() );
if ( empty( $ids ) ) {
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
/**
* UPDATED BEHAVIOR (v1.8.4):
* - On a parent product_cat archive: show only the direct children that actually have matching products.
* - On a sub-category archive: show only that sub-category itself (no siblings).
* - Outside product_cat contexts: fallback to original "all categories from scanned products".
*/
$cats = array();
$did_scope_to_children = false;
if ( $ctx_tax === 'product_cat' && ! empty( $ctx_terms ) && count( $ctx_terms ) === 1 ) {
$current_term = get_term_by( 'slug', $ctx_terms[0], 'product_cat' );
if ( $current_term && ! is_wp_error( $current_term ) ) {
// SUB-CATEGORY: show only this sub-category name
if ( isset( $current_term->parent ) && (int) $current_term->parent > 0 ) {
$cats = array( $current_term );
$did_scope_to_children = true;
} else {
// PARENT CATEGORY: show only direct children with products
$args = array(
'taxonomy' => 'product_cat',
'parent' => (int) $current_term->term_id,
'orderby' => 'name',
'order' => 'ASC',
'hide_empty' => true,
);
if ( ! empty( $ids ) ) {
$args['object_ids'] = $ids; // bind to matched products
}
$children = get_terms( $args );
if ( ! is_wp_error( $children ) && ! empty( $children ) ) {
$cats = $children;
$did_scope_to_children = true;
} else {
// No children with products -> empty list (UI shows empty-state)
$cats = array();
$did_scope_to_children = true;
}
}
}
}
// Fallback ONLY when not on product_cat context
if ( ! $did_scope_to_children ) {
$cats = wp_get_object_terms( $ids, 'product_cat', array( 'hide_empty' => false ) );
}
if ( ! is_wp_error( $cats ) && $cats ) $out['cats'] = $cats;
if ( $brand_tax ) {
$brands = wp_get_object_terms( $ids, $brand_tax, array( 'hide_empty' => false ) );
if ( ! is_wp_error( $brands ) && $brands ) $out['brands'] = $brands;
}
$attr_tax_slugs = array_values( array_unique( (array) $attr_tax_slugs ) );
foreach ( $attr_tax_slugs as $tax ) {
$terms = wp_get_object_terms( $ids, $tax, array( 'hide_empty' => false ) );
if ( ! is_wp_error( $terms ) && $terms ) $out['attrs'][ $tax ] = $terms;
}
set_transient( $cache_key, $out, (int) apply_filters( 'al_pf_terms_ttl', 30 ) );
if ( ! function_exists( 'alpf_index_transient' ) ) { function alpf_index_transient( $k ){} }
alpf_index_transient( $cache_key );
return $out;
}
}
/** =========================
* Shortcode (UI)
* =========================
* query_id, param_ns, source, label overrides...
*/
if ( ! function_exists( 'al_pf_filter_bar_shortcode' ) ) {
function al_pf_filter_bar_shortcode( $atts ) {
if ( ! class_exists( 'WooCommerce' ) ) return '<!-- WooCommerce not active -->';
$atts = shortcode_atts( array(
'brand_tax' => '',
'attributes' => 'all',
'show_reset' => 'yes',
'target' => '',
'query_id' => '',
'param_ns' => '',
'source' => 'all', // 'all' (default) or 'sale'
'label_category' => '',
'label_brand' => '',
'label_attributes' => '',
'label_reset' => '',
), $atts, 'wc_filter_bar' );
// Register custom Query IDs
$query_ids = array();
if ( ! empty( $atts['query_id'] ) ) {
foreach ( array_map( 'trim', explode( ',', (string) $atts['query_id'] ) ) as $qid ) {
$k = sanitize_key( $qid );
if ( $k ) $query_ids[] = $k;
}
if ( $query_ids ) al_pf_register_elementor_query_id( $query_ids );
}
// Determine namespace for this instance (param_ns > first query_id > '')
$ns = '';
if ( ! empty( $atts['param_ns'] ) ) {
$ns = sanitize_key( $atts['param_ns'] );
} elseif ( ! empty( $query_ids ) ) {
$ns = $query_ids[0];
}
// Auto-target: if no 'target' given but we have a single query_id, assume CSS ID equals query_id
$target_attr = trim( (string) $atts['target'] );
if ( ! $target_attr && ! empty( $query_ids ) ) {
$target_attr = '#' . $query_ids[0];
}
$forced_brand_tax = $atts['brand_tax'] ? sanitize_key( $atts['brand_tax'] ) : '';
$brand_tax = al_pf_get_brand_taxonomy( $forced_brand_tax );
$attr_tax_slugs = array();
if ( 'all' === strtolower( $atts['attributes'] ) ) {
if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
$global_attrs = wc_get_attribute_taxonomies();
if ( $global_attrs ) {
foreach ( $global_attrs as $ga ) {
$tax = 'pa_' . $ga->attribute_name;
if ( taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
}
} else {
foreach ( explode( ',', $atts['attributes'] ) as $maybe ) {
$tax = sanitize_key( trim( $maybe ) );
if ( $tax && taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
$attr_tax_slugs = array_values( array_unique( $attr_tax_slugs ) );
// Context (taxonomy archive)
$ctx_tax = '';
$ctx_terms = array();
if ( function_exists('is_tax') && is_tax() ) {
$qo = get_queried_object();
if ( $qo && ! empty( $qo->taxonomy ) && taxonomy_exists( $qo->taxonomy ) && is_object_in_taxonomy( 'product', $qo->taxonomy ) ) {
$ctx_tax = $qo->taxonomy;
$ctx_terms = array( $qo->slug );
}
}
$present = al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, array(
'tax' => $ctx_tax,
'terms' => $ctx_terms,
), $ns );
$sel_cat_s = alpf_get_param( 'pf_cat', $ns );
$sel_brand_s = alpf_get_param( 'pf_brand',$ns );
$sel_attr_s = alpf_get_param( 'pf_attr', $ns );
$sel_cats = $sel_cat_s ? array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $sel_cat_s ) ) ) ) : array();
$sel_brands = $sel_brand_s? array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) $sel_brand_s ) ) ) ) : array();
$sel_attrs = $sel_attr_s ? array_filter( array_map( 'sanitize_key', array_map( 'trim', explode( ',', (string) $sel_attr_s ) ) ) ) : array();
$sel_brand_tax = $brand_tax ? $brand_tax : ( alpf_get_param( 'pf_brand_tax', $ns ) ? sanitize_key( wp_unslash( alpf_get_param( 'pf_brand_tax', $ns ) ) ) : '' );
$search_term = alpf_get_param( 's', $ns );
$search_term = $search_term ? sanitize_text_field( wp_unslash( $search_term ) ) : get_search_query();
$attr_total_terms = 0;
foreach ( (array) $present['attrs'] as $terms ) { if ( is_array( $terms ) ) $attr_total_terms += count( $terms ); }
$instance_id = 'alpf-' . wp_generate_uuid4();
$nonce = wp_create_nonce( 'alpf_terms' );
// ARIA ids
$cats_dd_id = $instance_id . '-cats';
$brand_dd_id = $instance_id . '-brands';
$attrs_dd_id = $instance_id . '-attrs';
// Label overrides (i18n)
$label_category = $atts['label_category'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_category'] ) ) : __( 'Category', 'al-woo-ele-filter' );
$label_brand = $atts['label_brand'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_brand'] ) ) : __( 'Brand', 'al-woo-ele-filter' );
$label_attributes = $atts['label_attributes'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_attributes'] ) ) : __( 'Attributes', 'al-woo-ele-filter' );
$label_reset = $atts['label_reset'] !== '' ? sanitize_text_field( wp_unslash( $atts['label_reset'] ) ) : __( 'Reset', 'al-woo-ele-filter' );
$empty_msg = __( 'Nincs lehetőség a jelenlegi kiválasztáshoz.', 'al-woo-ele-filter' );
// Source handling
$source_attr = strtolower( sanitize_key( $atts['source'] ) );
if ( ! in_array( $source_attr, array( 'all', 'sale' ), true ) ) $source_attr = 'all';
$effective_source = alpf_get_effective_source( $ns );
$resolved_source = ( $source_attr !== 'all' ) ? $source_attr : $effective_source;
ob_start(); ?>
<div class="al-pf-wrap"
data-pf
id="<?php echo esc_attr( $instance_id ); ?>"
data-target="<?php echo esc_attr( $target_attr ); ?>"
data-expose="<?php echo esc_attr( implode( ',', $attr_tax_slugs ) ); ?>"
data-nonce="<?php echo esc_attr( $nonce ); ?>"
data-ctx-tax="<?php echo esc_attr( $ctx_tax ); ?>"
data-ctx-terms="<?php echo esc_attr( implode( ',', $ctx_terms ) ); ?>"
<?php if ( $ns ) : ?> data-ns="<?php echo esc_attr( $ns ); ?>"<?php endif; ?>>
<form class="al-pf-form" onsubmit="return false;" role="search" aria-label="<?php echo esc_attr__( 'Product filters', 'al-woo-ele-filter' ); ?>">
<?php if ( $search_term ) : ?>
<input type="hidden" data-param="s" value="<?php echo esc_attr( $search_term ); ?>">
<?php endif; ?>
<?php if ( $sel_brand_tax ) : ?>
<input type="hidden" data-param="pf_brand_tax" value="<?php echo esc_attr( $sel_brand_tax ); ?>">
<?php endif; ?>
<?php if ( $resolved_source === 'sale' ) : ?>
<input type="hidden" data-param="pf_source" value="sale">
<?php endif; ?>
<!-- Category -->
<details class="al-pf-dd" data-group="pf_cat" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($cats_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_category ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($cats_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_category ); ?></span>
<em class="al-pf-summary" data-summary="pf_cat"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($cats_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_category ); ?>">
<div class="al-pf-empty" data-empty="pf_cat" <?php echo empty($present['cats']) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-menu" <?php echo empty($present['cats']) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php if ( ! empty( $present['cats'] ) ) : foreach ( $present['cats'] as $t ) :
$slug = $t->slug; $checked = in_array( $slug, $sel_cats, true ); ?>
<label class="al-pf-option">
<input type="checkbox" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $checked ); ?> data-param="pf_cat">
<span><?php echo esc_html( $t->name ); ?></span>
</label>
<?php endforeach; endif; ?>
</div>
</div>
</details>
<!-- Brand -->
<?php if ( $sel_brand_tax ) : ?>
<details class="al-pf-dd" data-group="pf_brand" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($brand_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_brand ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($brand_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_brand ); ?></span>
<em class="al-pf-summary" data-summary="pf_brand"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($brand_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_brand ); ?>">
<div class="al-pf-empty" data-empty="pf_brand" <?php echo empty($present['brands']) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-menu" <?php echo empty($present['brands']) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php if ( ! empty( $present['brands'] ) ) : foreach ( $present['brands'] as $b ) :
$slug = $b->slug;
$checked = in_array( $slug, $sel_brands, true );
$logo = al_pf_get_term_logo_url( $b->term_id );
?>
<label class="al-pf-option al-pf-brand-option">
<input type="checkbox" value="<?php echo esc_attr( $slug ); ?>" <?php checked( $checked ); ?> data-param="pf_brand">
<?php if ( $logo ) : ?>
<img class="al-pf-brand-thumb" src="<?php echo esc_url( $logo ); ?>" alt="" loading="lazy" decoding="async">
<?php endif; ?>
<span><?php echo esc_html( $b->name ); ?></span>
</label>
<?php endforeach; endif; ?>
</div>
</div>
</details>
<?php endif; ?>
<!-- Attributes -->
<details class="al-pf-dd" data-group="pf_attr_all" role="listbox" aria-multiselectable="true" id="<?php echo esc_attr($attrs_dd_id); ?>">
<summary class="al-pf-trigger" aria-label="<?php echo esc_attr( $label_attributes ); ?>" aria-expanded="false" aria-controls="<?php echo esc_attr($attrs_dd_id); ?>-panel">
<span class="al-pf-labeltxt"><?php echo esc_html( $label_attributes ); ?></span>
<em class="al-pf-summary" data-summary="pf_attr_all"></em>
</summary>
<div class="al-pf-panel" id="<?php echo esc_attr($attrs_dd_id); ?>-panel" role="group" aria-label="<?php echo esc_attr( $label_attributes ); ?>">
<div class="al-pf-empty" data-empty="pf_attr_all" <?php echo ($attr_total_terms===0) ? '' : 'hidden aria-hidden="true"'; ?>>
<?php echo esc_html( $empty_msg ); ?>
</div>
<div class="al-pf-attr-groups" <?php echo ($attr_total_terms===0) ? 'hidden aria-hidden="true"' : ''; ?>>
<?php foreach ( $present['attrs'] as $tax => $terms ) :
if ( empty( $terms ) ) continue; ?>
<section class="al-pf-attr-group" data-attr-tax="<?php echo esc_attr( $tax ); ?>">
<h4 class="al-pf-group-title"><?php echo esc_html( function_exists('wc_attribute_label') ? wc_attribute_label( $tax ) : $tax ); ?></h4>
<div class="al-pf-grid">
<?php foreach ( $terms as $term ) :
$val = $tax . ':' . $term->slug;
$checked = in_array( $val, $sel_attrs, true ); ?>
<label class="al-pf-option">
<input type="checkbox" value="<?php echo esc_attr( $val ); ?>" <?php checked( $checked ); ?> data-param="pf_attr" data-tax="<?php echo esc_attr( $tax ); ?>">
<span><?php echo esc_html( $term->name ); ?></span>
</label>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
</details>
<?php if ( 'yes' === strtolower( $atts['show_reset'] ) ) : ?>
<button type="button" class="al-pf-reset" aria-label="<?php echo esc_attr( $label_reset ); ?>"><?php echo esc_html( $label_reset ); ?></button>
<?php endif; ?>
</form>
</div>
<style>
/* Add your CSS if needed */
</style>
<script>
(function(){
window.__alpfInstances = window.__alpfInstances || [];
if (!window.CSS || typeof CSS.escape !== 'function') {
window.CSS = window.CSS || {};
CSS.escape = CSS.escape || function(value){ return String(value).replace(/[^a-zA-Z0-9_\\-]/g, '\\\\$&'); };
}
// Simple SR live region for a11y announcements
var __alpfSR = document.getElementById('alpf-sr');
if (!__alpfSR) {
__alpfSR = document.createElement('div');
__alpfSR.id = 'alpf-sr';
__alpfSR.setAttribute('aria-live', 'polite');
__alpfSR.style.position='absolute'; __alpfSR.style.width='1px'; __alpfSR.style.height='1px';
__alpfSR.style.margin='-1px'; __alpfSR.style.border='0'; __alpfSR.style.padding='0';
__alpfSR.style.clip='rect(0 0 0 0)'; __alpfSR.style.overflow='hidden';
document.body.appendChild(__alpfSR);
}
function vw(){ return Math.max(document.documentElement.clientWidth||0, window.innerWidth||0); }
function vh(){ return Math.max(document.documentElement.clientHeight||0, window.innerHeight||0); }
document.querySelectorAll('[data-pf]').forEach(function(root){
if (root.__alpfInit) return;
root.__alpfInit = true;
var targetSelector = root.getAttribute('data-target') || '';
var exposeAttrsCsv = root.getAttribute('data-expose') || '';
var ajaxNonce = root.getAttribute('data-nonce') || '';
var ctxTax = root.getAttribute('data-ctx-tax') || '';
var ctxTermsCsv = root.getAttribute('data-ctx-terms') || '';
var ns = root.getAttribute('data-ns') || ''; // NAMESPACE
var AJAX_URL = '<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>';
function nsKey(k){ return ns ? (ns + '_' + k) : k; }
/* UX helpers */
function smoothShow(el){ if(!el) return; el.hidden=false; el.setAttribute('aria-hidden','false'); el.style.opacity='0'; el.style.transform='scale(0.98)'; requestAnimationFrame(function(){ el.style.transition='opacity .18s ease, transform .18s ease'; el.style.opacity='1'; el.style.transform='scale(1)'; }); setTimeout(function(){ el.style.transition=''; el.style.opacity=''; el.style.transform=''; },220); }
function smoothHide(el, after){ if(!el||el.hidden){ if(after) after(); return; } if(el.tagName.toLowerCase()==='details') el.removeAttribute('open'); el.style.transition='opacity .18s ease, transform .18s ease'; el.style.opacity='0'; el.style.transform='scale(0.98)'; setTimeout(function(){ el.hidden=true; el.setAttribute('aria-hidden','true'); el.style.transition=''; el.style.opacity=''; el.style.transform=''; if(after) after(); },200); }
function removeWithFade(el){ if(!el) return; smoothHide(el, function(){ if(el&&el.parentNode) el.parentNode.removeChild(el); }); }
function getCheckedValues(param, scope){
var sel=scope||root;
var inputs=sel.querySelectorAll('input[type="checkbox"][data-param="'+param+'"]:checked');
var vals=[]; inputs.forEach(function(i){ vals.push(i.value); });
return vals;
}
var applyTimer=null; function scheduleApply(){ clearTimeout(applyTimer); applyTimer=setTimeout(function(){ setSummaryText(); applyAjax(true); }, 160); }
function setSummaryText(){
function t(n){return n>0?'('+n+')':'';}
var a=root.querySelector('[data-summary="pf_cat"]'); if(a){a.textContent=t(getCheckedValues('pf_cat').length);}
var b=root.querySelector('[data-summary="pf_brand"]'); if(b){b.textContent=t(getCheckedValues('pf_brand').length);}
var c=root.querySelector('[data-summary="pf_attr_all"]'); if(c){c.textContent=t(getCheckedValues('pf_attr').length);}
}
function applyControlsFromUrl(rootEl, url){
var params=(url instanceof URL)?url.searchParams:new URL(url||window.location.href).searchParams;
function csv(k){ var v=params.get(nsKey(k))||''; return v? v.split(',').map(function(s){ return s.trim(); }).filter(Boolean):[]; }
var cats=csv('pf_cat'), brands=csv('pf_brand'), attrs=csv('pf_attr');
rootEl.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked=false; });
cats.forEach(function(slug){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_cat"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
brands.forEach(function(slug){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_brand"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
attrs.forEach(function(pair){ var cb=rootEl.querySelector('input[type="checkbox"][data-param="pf_attr"][value="'+CSS.escape(pair)+'"]'); if(cb) cb.checked=true; });
}
function buildUrl(baseHref, opts){
opts = opts || {};
var url=new URL(baseHref||window.location.href); var params=url.searchParams;
root.querySelectorAll('[data-param="s"], [data-param="pf_brand_tax"], [data-param="pf_source"]').forEach(function(h){
var key=h.getAttribute('data-param'), val=h.value||'';
val ? params.set(nsKey(key), val) : params.delete(nsKey(key));
});
var cats=getCheckedValues('pf_cat'); cats.length?params.set(nsKey('pf_cat'),cats.join(',')):params.delete(nsKey('pf_cat'));
var br=getCheckedValues('pf_brand'); br.length?params.set(nsKey('pf_brand'),br.join(',')):params.delete(nsKey('pf_brand'));
var at=getCheckedValues('pf_attr'); at.length?params.set(nsKey('pf_attr'),at.join(',')):params.delete(nsKey('pf_attr'));
// Keep orderby if present (namespaced as well)
var curr=new URL(window.location.href);
var ob = curr.searchParams.get(nsKey('orderby'));
if(!params.has(nsKey('orderby')) && ob) params.set(nsKey('orderby'), ob);
if (opts.resetPage) {
['paged','page','product-page'].forEach(function(k){ params.delete(k); });
}
url.search=params.toString(); return url;
}
// ===== Robust widget + container detection =====
function findTargetPair(doc){
var d = doc || document;
var wrapper = null, container = null;
var widgetSelectors = [
'.elementor-widget-loop-grid',
'.elementor-widget-wc-archive-products',
'[data-widget_type*="loop-grid"]',
'[data-element_type="widget"][class*="loop-grid"]'
];
var containerSelectors = [
'.elementor-loop-container',
'.e-loop-container',
'ul.products',
'.woocommerce ul.products',
'.elementor-widget-wc-archive-products .products',
'.woocommerce-products',
'.products',
'.woocommerce .products'
];
function first(scope, sels){
for (var i=0;i<sels.length;i++){
var m = scope.querySelector ? scope.querySelector(sels[i]) : null;
if (m) return m;
}
return null;
}
if (targetSelector) {
var t = d.querySelector(targetSelector);
if (t) {
if (t.matches && (t.matches(widgetSelectors.join(',')))) {
wrapper = t;
} else {
wrapper = first(t, widgetSelectors) || null;
}
if (wrapper) {
container = first(wrapper, containerSelectors) || wrapper.querySelector('ul.products');
} else {
container = first(t, containerSelectors);
}
}
}
if (!wrapper) wrapper = first(d, widgetSelectors);
if (!container) {
if (wrapper) container = first(wrapper, containerSelectors);
if (!container) container = first(d, containerSelectors);
}
return { wrapper: wrapper || null, container: container || null };
}
function setBusy(hostEl, busy){
if (!hostEl) return;
if (!hostEl.classList.contains('alpf-overlay-host')) {
var cs = getComputedStyle(hostEl);
if (cs.position === 'static') hostEl.classList.add('alpf-overlay-host');
}
if (busy) {
if (!hostEl.classList.contains('alpf-target-busy')) {
hostEl.classList.add('alpf-target-busy');
hostEl.setAttribute('aria-busy','true');
var overlay = document.createElement('div');
overlay.className = 'alpf-preloader';
overlay.setAttribute('role','status');
overlay.setAttribute('aria-live','polite');
var label = '<?php echo esc_js( __( 'Loading products…', 'al-woo-ele-filter' ) ); ?>';
overlay.innerHTML = '<div class="alpf-preloader-inner"><div class="alpf-loader" aria-hidden="true"></div><div class="alpf-label">'+ label +'</div></div>';
hostEl.appendChild(overlay);
requestAnimationFrame(function(){ overlay.classList.add('alpf-show'); });
}
} else {
hostEl.classList.remove('alpf-target-busy');
hostEl.removeAttribute('aria-busy');
var overlay = hostEl.querySelector('.alpf-preloader');
if (overlay) { overlay.classList.remove('alpf-show'); setTimeout(function(){ overlay && overlay.remove(); }, 180); }
}
}
function rehydrateScripts(scope){
if(!scope) return;
var scripts = scope.querySelectorAll('script');
scripts.forEach(function(oldS){
var s = document.createElement('script');
for (var i=0;i<oldS.attributes.length;i++) { var a = oldS.attributes[i]; s.setAttribute(a.name, a.value); }
s.textContent = oldS.textContent || '';
oldS.parentNode.replaceChild(s, oldS);
});
}
function rehydrateElementor(scope){
if (window.elementorFrontend) {
try {
var $scope = window.jQuery ? window.jQuery(scope) : null;
if (elementorFrontend.elementsHandler && elementorFrontend.elementsHandler.runReadyTrigger) elementorFrontend.elementsHandler.runReadyTrigger($scope || scope);
if (elementorFrontend.hooks && elementorFrontend.hooks.doAction) {
elementorFrontend.hooks.doAction('frontend/element_ready/global', $scope || scope);
elementorFrontend.hooks.doAction('frontend/element_ready/loop-grid.default', $scope || scope);
elementorFrontend.hooks.doAction('frontend/element_ready/woocommerce-archive-products.default', $scope || scope);
}
if (elementorFrontend.elements && elementorFrontend.elements.$window) elementorFrontend.elements.$window.trigger('resize');
} catch(e){ console.warn('[al-pf] Elementor re-init failed:', e); }
}
}
function setEmptyState(groupKey, hasItems) {
var panel = root.querySelector('[data-group="'+groupKey+'"] .al-pf-panel');
if (!panel) return;
var emptyEl = panel.querySelector('.al-pf-empty[data-empty="'+groupKey+'"]');
var listEl = panel.querySelector(groupKey === 'pf_attr_all' ? '.al-pf-attr-groups' : '.al-pf-menu');
if (emptyEl) {
emptyEl.hidden = !!hasItems;
emptyEl.setAttribute('aria-hidden', hasItems ? 'true' : 'false');
}
if (listEl) {
listEl.hidden = !hasItems;
listEl.setAttribute('aria-hidden', hasItems ? 'false' : 'true');
}
}
function rebuildCheckboxList(container, param, items) {
var selected = new Set(getCheckedValues(param));
container.innerHTML = '';
var hasItems = !!(items && items.length);
if (!hasItems) {
setEmptyState(param, false);
return;
}
items.forEach(function(it){
var label = document.createElement('label');
label.className = 'al-pf-option' + (param === 'pf_brand' ? ' al-pf-brand-option' : '');
var input = document.createElement('input');
input.type = 'checkbox';
input.setAttribute('data-param', param);
input.value = it.slug;
if (selected.has(it.slug)) input.checked = true;
input.addEventListener('change', scheduleApply);
label.appendChild(input);
if (param === 'pf_brand' && it.logo) {
var img = document.createElement('img');
img.className = 'al-pf-brand-thumb';
img.src = it.logo;
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
label.appendChild(img);
}
var span = document.createElement('span');
span.textContent = it.name;
label.appendChild(span);
container.appendChild(label);
});
setEmptyState(param, true);
}
async function refreshFilterOptions(urlObj){
var u = new URL(AJAX_URL, window.location.origin);
var activeUrl = (urlObj instanceof URL) ? urlObj : new URL(window.location.href);
u.search = activeUrl.search;
u.searchParams.set('action','alpf_terms');
if (exposeAttrsCsv) u.searchParams.set('expose_attrs', exposeAttrsCsv);
if (ajaxNonce) u.searchParams.set('security', ajaxNonce);
if (ctxTax) u.searchParams.set('ctx_tax', ctxTax);
if (ctxTermsCsv) u.searchParams.set('ctx_terms', ctxTermsCsv);
if (ns) u.searchParams.set('ns', ns); // pass namespace to server
try{
var res = await fetch(u.toString(), { credentials:'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if (!res.ok) throw new Error('HTTP '+res.status);
var j = await res.json();
if (!j || !j.success) return;
var data = j.data || {};
var attrLabels = data.attr_labels || {};
var catDetails = root.querySelector('[data-group="pf_cat"]');
var catMenu = root.querySelector('[data-group="pf_cat"] .al-pf-menu');
if (catDetails && catMenu) {
rebuildCheckboxList(catMenu, 'pf_cat', data.cats || []);
}
var brandDetails = root.querySelector('[data-group="pf_brand"]');
var brandMenu = root.querySelector('[data-group="pf_brand"] .al-pf-menu');
if (brandDetails && brandMenu) {
rebuildCheckboxList(brandMenu, 'pf_brand', data.brands || []);
}
// Remove empty attribute sections; we still keep the dropdown visible with empty message
Array.from(root.querySelectorAll('.al-pf-attr-group')).forEach(function(sec){
var tax = sec.getAttribute('data-attr-tax') || '';
var items = (data.attrs && data.attrs[tax]) ? data.attrs[tax] : [];
if (!items.length) removeWithFade(sec);
});
function ensureAttrSection(tax, labelText){
var groupsWrap = root.querySelector('.al-pf-attr-groups');
if (!groupsWrap) return null;
var section = root.querySelector('.al-pf-attr-group[data-attr-tax="'+CSS.escape(tax)+'"]');
if (section) return section;
section = document.createElement('section');
section.className = 'al-pf-attr-group';
section.setAttribute('data-attr-tax', tax);
var h4 = document.createElement('h4'); h4.className = 'al-pf-group-title'; h4.textContent = labelText || tax;
var grid = document.createElement('div'); grid.className = 'al-pf-grid';
section.appendChild(h4); section.appendChild(grid);
groupsWrap.appendChild(section);
return section;
}
function rebuildAttrGroup(section, tax, items){
var grid = section.querySelector('.al-pf-grid');
var selected = new Set(getCheckedValues('pf_attr'));
grid.innerHTML = '';
if (!items || !items.length) return;
items.forEach(function(it){
var val = tax + ':' + it.slug;
var label = document.createElement('label'); label.className='al-pf-option';
var input = document.createElement('input');
input.type='checkbox'; input.setAttribute('data-param','pf_attr'); input.setAttribute('data-tax',tax); input.value = val;
if (selected.has(val)) input.checked = true;
input.addEventListener('change', scheduleApply);
var span = document.createElement('span'); span.textContent = it.name;
label.appendChild(input); label.appendChild(span);
grid.appendChild(label);
});
}
(Object.keys(data.attrs||{})).forEach(function(tax){
var items = (data.attrs && data.attrs[tax]) ? data.attrs[tax] : [];
if (!items.length) return;
var labelText = (attrLabels && attrLabels[tax]) ? attrLabels[tax] : tax;
var section = ensureAttrSection(tax, labelText);
if (!section) return;
rebuildAttrGroup(section, tax, items);
section.hidden=false; section.setAttribute('aria-hidden','false');
section.style.opacity='0'; section.style.transform='scale(0.98)';
requestAnimationFrame(function(){
section.style.transition='opacity .18s ease, transform .18s ease';
section.style.opacity='1'; section.style.transform='scale(1)';
setTimeout(function(){ section.style.transition=''; section.style.opacity=''; section.style.transform=''; },220);
});
});
var anyAttrItems = data.attrs && Object.keys(data.attrs).some(function(t){ return (data.attrs[t]||[]).length; });
setEmptyState('pf_attr_all', !!anyAttrItems);
setSummaryText();
} catch(err){ console.warn('[al-pf] terms refresh failed:', err); }
}
async function fetchAndSwap(url){
var cur = findTargetPair(document);
var busyHost = cur.wrapper || cur.container;
if (!busyHost) { window.location.href = url.toString(); return; }
setBusy(busyHost, true);
var safetyTimer = setTimeout(function(){ setBusy(busyHost, false); }, 8000);
try {
var xhrUrl = new URL(url.toString());
xhrUrl.searchParams.set('_alpf', '1');
var res = await fetch(xhrUrl.toString(), {
credentials:'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!res.ok) throw new Error('HTTP '+res.status);
var html = await res.text();
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var neu = findTargetPair(doc);
var newWrapper = neu.wrapper;
var newContainer = neu.container;
if (!newWrapper && !newContainer) throw new Error('Target nodes not found in response');
var didReplaceWrapper = false;
if (cur.wrapper && newWrapper) {
var tmp = document.createElement('div');
tmp.innerHTML = newWrapper.outerHTML;
var fresh = tmp.firstElementChild;
cur.wrapper.replaceWith(fresh);
var updated = findTargetPair(document);
didReplaceWrapper = true;
rehydrateScripts(updated.wrapper || updated.container);
rehydrateElementor(updated.wrapper || updated.container);
bindContainerInteractions(updated.container || updated.wrapper);
setBusy(updated.wrapper || updated.container, false);
} else if (cur.container && newContainer) {
cur.container.innerHTML = newContainer.innerHTML;
rehydrateScripts(cur.container);
rehydrateElementor(cur.container);
bindContainerInteractions(cur.container);
} else {
window.location.href = url.toString();
return;
}
refreshFilterOptions(url);
var region = (didReplaceWrapper ? (findTargetPair(document).wrapper || findTargetPair(document).container) : cur.container);
if (region) {
region.setAttribute('role','region');
region.setAttribute('aria-live','polite');
region.scrollIntoView({ behavior:'smooth', block:'start' });
}
__alpfSR.textContent = '<?php echo esc_js( __( 'Products updated', 'al-woo-ele-filter' ) ); ?>';
document.dispatchEvent(new CustomEvent('alpf:afterSwap', { detail: { container: region, url: url.toString() } }));
if (window.jQuery) {
jQuery(document.body).trigger('alpf_after_swap', [ region, url.toString() ]);
jQuery(document.body).trigger('updated_wc_div');
jQuery(document.body).trigger('wc_fragment_refresh');
jQuery(document.body).trigger('init_price_filter');
jQuery(document.body).trigger('wc_update_cart');
}
} catch(err){
console.error('[al-pf] AJAX swap failed:', err);
window.location.href = url.toString();
} finally {
clearTimeout(safetyTimer);
var latest = findTargetPair(document);
setBusy(latest.wrapper || latest.container || busyHost, false);
}
}
function applyAjax(filtersChanged){
var url = buildUrl(undefined, { resetPage: !!filtersChanged });
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf: 1, href: href }, '', href);
}
fetchAndSwap(url);
if (filtersChanged) closeAll(null);
}
// Close all dropdowns except the provided one
function closeAll(except){
root.querySelectorAll('.al-pf-dd[open]').forEach(function(dd){ if (dd !== except) dd.removeAttribute('open'); });
}
// ===== Mobile viewport clamping / gap fix (no overlay) =====
function fitPanel(dd){
if (!dd) return;
var panel = dd.querySelector('.al-pf-panel'); if (!panel) return;
// Reset positioning
panel.classList.remove('mobile-fit');
panel.style.position=''; panel.style.left=''; panel.style.right=''; panel.style.top=''; panel.style.bottom='';
panel.style.maxHeight=''; panel.style.width=''; panel.style.maxWidth=''; panel.classList.remove('fit-clamped');
dd.classList.remove('flip');
var viewportW = vw();
var viewportH = vh();
var GAP = 10; // ensure there's always a visual gap from the opener
// Small screens: fixed sheet without overlay; clamp and add gap so it never overlaps opener
if (viewportW <= 480) {
var ddRect = dd.getBoundingClientRect();
panel.classList.add('mobile-fit');
panel.style.left = '8px';
panel.style.right = '8px';
panel.style.maxHeight = Math.round(viewportH * 0.7) + 'px';
// Measure current height (after maxHeight applied)
var ph = panel.getBoundingClientRect().height || Math.round(viewportH * 0.7);
var spaceBelow = viewportH - ddRect.bottom - GAP;
var spaceAbove = ddRect.top - GAP;
if (spaceBelow >= Math.min(ph, viewportH * 0.4)) {
// Open below, with gap -> no overlap with opener
var top = Math.min(ddRect.bottom + GAP, viewportH - GAP - Math.min(ph, Math.round(viewportH * 0.7)));
panel.style.top = top + 'px';
} else {
// Open above, with gap
var topAbove = Math.max(GAP, ddRect.top - GAP - Math.min(ph, Math.round(viewportH * 0.7)));
panel.style.top = topAbove + 'px';
}
return;
}
// Desktop/tablet: absolute positioning with clamping and flip
var vwPadding = 12;
var maxW = Math.min(1200, viewportW - (vwPadding*2));
var natural = Math.ceil(panel.scrollWidth + 20);
var minW = Math.max(220, Math.min(340, viewportW - 2*vwPadding));
var finalW = Math.max(minW, Math.min(natural, maxW));
panel.style.maxWidth = Math.round(maxW) + 'px';
panel.style.width = Math.round(finalW) + 'px';
panel.style.left='0px'; panel.style.right='auto';
var rect=panel.getBoundingClientRect();
if (rect.right > viewportW - vwPadding) { panel.style.left='auto'; panel.style.right='0px'; }
var ddRect=dd.getBoundingClientRect(); var spaceBelow=viewportH-ddRect.bottom; var spaceAbove=ddRect.top;
if (spaceBelow < rect.height && spaceAbove > spaceBelow) dd.classList.add('flip');
// After flip, re-measure and correct horizontal overflow
rect = panel.getBoundingClientRect();
if (rect.left < vwPadding) {
panel.style.left = (vwPadding - (ddRect.left)) + 'px';
panel.style.right = 'auto';
}
rect = panel.getBoundingClientRect();
if (rect.right > viewportW - vwPadding) {
panel.style.left = 'auto';
panel.style.right = (vwPadding) + 'px';
}
}
function bindContainerInteractions(container){
if (!container) return;
if (!container.dataset.alpfPagBound) {
container.addEventListener('click', function(e){
var a = e.target.closest('.page-numbers a, a.page-numbers, .woocommerce-pagination a');
if (!a) return;
e.preventDefault();
var url = new URL(a.href);
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
}, { passive:false });
container.dataset.alpfPagBound = '1';
}
var orderForm = container.querySelector('form.woocommerce-ordering');
var select = orderForm ? orderForm.querySelector('select[name="orderby"]') : null;
if (!select) {
var globalForm = document.querySelector('form.woocommerce-ordering');
if (globalForm) select = globalForm.querySelector('select[name="orderby"]');
}
if (select && !select.dataset.alpfBound) {
select.addEventListener('change', function(){
var url = buildUrl(undefined, { resetPage: true });
url.searchParams.set(nsKey('orderby'), select.value); // namespaced orderby
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
});
select.dataset.alpfBound = '1';
}
}
// Toggle behavior + events (enforce single-open)
root.querySelectorAll('.al-pf-dd').forEach(function(dd){
var summary = dd.querySelector('.al-pf-trigger');
// Pre-close siblings BEFORE the browser toggles (pointer/keyboard)
if (summary) {
summary.addEventListener('pointerdown', function(){
if (!dd.open) closeAll(dd);
});
summary.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') {
if (!dd.open) closeAll(dd);
}
});
}
// Fallback when toggle completes
dd.addEventListener('toggle', function(){
if (summary) summary.setAttribute('aria-expanded', dd.open ? 'true' : 'false');
if (dd.open) {
closeAll(dd);
fitPanel(dd);
}
});
// Close on ESC
dd.addEventListener('keydown', function(e){
if (e.key === 'Escape') dd.removeAttribute('open');
});
});
// Refit on resize/orientation/scroll (helps fixed mobile sheet stay aligned)
var refit = function(){
var openDD = root.querySelector('.al-pf-dd[open]');
if (openDD) fitPanel(openDD);
};
window.addEventListener('resize', refit, { passive:true });
window.addEventListener('orientationchange', refit, { passive:true });
window.addEventListener('scroll', function(){
if (document.querySelector('.al-pf-panel.mobile-fit')) refit();
}, { passive:true });
document.addEventListener('click', function(e){
var isInside = e.target.closest ? e.target.closest('.al-pf-dd') : null;
if (isInside && root.contains(isInside)) return;
if (!root.contains(e.target)) closeAll(null);
});
// Change listeners
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.addEventListener('change', scheduleApply); });
// Reset
var resetBtn = root.querySelector('.al-pf-reset');
if (resetBtn){
resetBtn.addEventListener('click', function(){
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked = false; });
setSummaryText();
var url = buildUrl(undefined, { resetPage: true });
url.searchParams.delete(nsKey('orderby'));
var href = url.toString();
if (href !== window.location.href) {
history.pushState({ alpf:1, href: href }, '', href);
}
fetchAndSwap(url);
closeAll(null);
});
}
// Initial
applyControlsFromUrl(root, window.location.href);
var initialPair = findTargetPair(document);
bindContainerInteractions(initialPair.container || initialPair.wrapper);
setSummaryText();
refreshFilterOptions(new URL(window.location.href));
if (initialPair.container || initialPair.wrapper) {
var scope = initialPair.wrapper || initialPair.container;
document.dispatchEvent(new CustomEvent('alpf:afterSwap', { detail: { container: scope, url: window.location.href } }));
if (window.jQuery) jQuery(document.body).trigger('alpf_after_swap', [ scope, window.location.href ]);
}
window.__alpfInstances.push({ root: root, refresh: function(){ var url = new URL(window.location.href); fetchAndSwap(url); } });
});
// History back/forward — honor namespace
window.addEventListener('popstate', function(){
if (!window.__alpfInstances) return;
var url = new URL(window.location.href);
window.__alpfInstances.forEach(function(i){
if (!i || !i.root) return;
var root=i.root, ns=root.getAttribute('data-ns')||'';
function nsKey(k){ return ns ? (ns+'_'+k) : k; }
function csv(k){ var v=url.searchParams.get(nsKey(k))||''; return v? v.split(',').map(function(s){return s.trim();}).filter(Boolean):[]; }
var cats=csv('pf_cat'), brands=csv('pf_brand'), attrs=csv('pf_attr');
root.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(cb){ cb.checked=false; });
cats.forEach(function(slug){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_cat"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
brands.forEach(function(slug){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_brand"][value="'+CSS.escape(slug)+'"]'); if(cb) cb.checked=true; });
attrs.forEach(function(pair){ var cb=root.querySelector('input[type="checkbox"][data-param="pf_attr"][value="'+CSS.escape(pair)+'"]'); if(cb) cb.checked=true; });
function gv(p){ return Array.from(root.querySelectorAll('input[type="checkbox"][data-param="'+p+'"]:checked')).map(function(n){return n.value;}); }
function t(n){return n>0?'('+n+')':'';}
var a=root.querySelector('[data-summary="pf_cat"]'); if(a){a.textContent=t(gv('pf_cat').length);}
var b=root.querySelector('[data-summary="pf_brand"]'); if(b){b.textContent=t(gv('pf_brand').length);}
var c=root.querySelector('[data-summary="pf_attr_all"]'); if(c){c.textContent=t(gv('pf_attr').length);}
if (typeof i.refresh === 'function') i.refresh();
});
});
})();
</script>
<?php
return ob_get_clean();
}
}
add_shortcode( 'wc_filter_bar', 'al_pf_filter_bar_shortcode' );
/** =========================
* Server-side query filters (main query)
* ========================= */
if ( ! function_exists( 'al_pf_filter_main_search_query' ) ) {
function al_pf_filter_main_search_query( $q ) {
if ( is_admin() || ! ( $q instanceof WP_Query ) || ! $q->is_main_query() ) return;
// Detect any namespaced or non-namespaced PF params
$has_pf_params = alpf_request_has_pf_params_globally();
$is_product_archive = function_exists('is_post_type_archive') && is_post_type_archive('product');
$is_product_tax = false;
if ( function_exists('get_object_taxonomies') && ! is_null( get_object_taxonomies( 'product' ) ) ) {
foreach ( get_object_taxonomies( 'product' ) as $ptax ) {
if ( function_exists('is_tax') && is_tax( $ptax ) ) { $is_product_tax = true; break; }
}
}
$is_product_pt = ( 'product' === $q->get('post_type') || ( is_array( $q->get('post_type') ) && in_array( 'product', (array) $q->get('post_type'), true ) ) );
if ( $q->is_search() && ! $has_pf_params && ! $is_product_archive && ! $is_product_tax && ! $is_product_pt ) return;
if ( $q->is_search() && $has_pf_params ) {
$q->set( 'post_type', array( 'product' ) );
}
if ( $has_pf_params || $is_product_archive || $is_product_tax || $is_product_pt ) {
$parts = al_pf_build_tax_query_parts( '', '' );
if ( $parts ) {
$tax_query = (array) $q->get( 'tax_query' );
$tax_query = array_merge( $tax_query, $parts );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
$q->set( 'tax_query', $tax_query );
}
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( $q->is_search(), '' );
$tax_query = array_merge( (array) $q->get('tax_query'), $guard_tax );
$meta_query = array_merge( (array) $q->get('meta_query'), $guard_meta );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query = array_merge( array( 'relation' => 'AND' ), $meta_query );
}
// Apply effective source globally (ns '')
$args = al_pf_apply_source_to_args( array(), '' );
if ( ! empty( $args['post__in'] ) ) {
$existing = (array) $q->get( 'post__in' );
if ( ! empty( $existing ) ) {
$q->set( 'post__in', array_values( array_intersect( array_map( 'intval', $existing ), array_map( 'intval', $args['post__in'] ) ) ) );
} else {
$q->set( 'post__in', $args['post__in'] );
}
}
$q->set( 'tax_query', $tax_query );
$q->set( 'meta_query', $meta_query );
}
}
}
add_action( 'pre_get_posts', 'al_pf_filter_main_search_query' );
/** =========================
* Elementor Loop Grid query — namespaced
* ========================= */
if ( ! function_exists( 'al_pf_elementor_posts_query' ) ) {
function al_pf_elementor_posts_query( $query ) {
if ( is_admin() ) return;
if ( ! ( $query instanceof WP_Query ) ) return;
$hook = current_filter();
$parts = explode( '/', (string) $hook );
$ns = sanitize_key( end( $parts ) );
$default_pp = (int) apply_filters( 'alpf_posts_per_page_default', 30 );
$pp = (int) $query->get( 'posts_per_page' );
if ( $pp < 1 ) $query->set( 'posts_per_page', $default_pp );
// namespaced orderby
$orderby_raw = alpf_get_param( 'orderby', $ns );
if ( $orderby_raw !== null && $orderby_raw !== '' ) {
$orderby = function_exists('wc_clean') ? wc_clean( wp_unslash( $orderby_raw ) ) : sanitize_text_field( wp_unslash( $orderby_raw ) );
al_pf_apply_elementor_orderby_map( $query, $orderby );
}
// Any namespaced filter/search or effective source default?
$effective_source = alpf_get_effective_source( $ns );
$has_params = (
alpf_get_param( 'pf_cat', $ns ) !== null ||
alpf_get_param( 'pf_brand', $ns ) !== null ||
alpf_get_param( 'pf_attr', $ns ) !== null ||
alpf_get_param( 'pf_brand_tax', $ns ) !== null ||
alpf_get_param( 's', $ns ) !== null ||
alpf_get_param( 'min_price', $ns ) !== null ||
alpf_get_param( 'max_price', $ns ) !== null ||
$effective_source !== ''
);
if ( ! $has_params ) return;
$pt = $query->get('post_type');
if ( $pt && $pt !== 'product' && ( ! is_array($pt) || ! in_array( 'product', (array) $pt, true ) ) ) return;
$parts = al_pf_build_tax_query_parts( '', $ns );
if ( $parts ) {
$tax_query = (array) $query->get( 'tax_query' );
$tax_query = array_merge( $tax_query, $parts );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
$query->set( 'tax_query', $tax_query );
}
$s_raw = alpf_get_param( 's', $ns );
if ( $s_raw !== null && $s_raw !== '' ) {
$query->set( 'post_type', array( 'product' ) );
$query->set( 's', sanitize_text_field( wp_unslash( $s_raw ) ) );
}
list( $guard_tax, $guard_meta ) = alpf_build_catalog_guards( $s_raw !== null && $s_raw !== '', $ns );
$tax_query = array_merge( (array) $query->get('tax_query'), $guard_tax );
$meta_query = array_merge( (array) $query->get('meta_query'), $guard_meta );
if ( count( $tax_query ) > 1 && ! isset( $tax_query['relation'] ) ) {
$tax_query = array_merge( array( 'relation' => 'AND' ), $tax_query );
}
if ( count( $meta_query ) > 1 && ! isset( $meta_query['relation'] ) ) {
$meta_query = array_merge( array( 'relation' => 'AND' ), $meta_query );
}
// Apply effective source (e.g., sale) for this namespace, intersect with existing post__in
$args = al_pf_apply_source_to_args( array(), $ns );
if ( ! empty( $args['post__in'] ) ) {
$existing = (array) $query->get( 'post__in' );
if ( ! empty( $existing ) ) {
$query->set( 'post__in', array_values( array_intersect( array_map( 'intval', $existing ), array_map( 'intval', $args['post__in'] ) ) ) );
} else {
$query->set( 'post__in', $args['post__in'] );
}
}
$query->set( 'tax_query', $tax_query );
$query->set( 'meta_query', $meta_query );
}
}
/** Woo 'orderby' map */
if ( ! function_exists( 'al_pf_apply_elementor_orderby_map' ) ) {
function al_pf_apply_elementor_orderby_map( WP_Query $query, $orderby ) {
switch ( $orderby ) {
case 'price':
$query->set( 'meta_key', '_price' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'ASC' ); break;
case 'price-desc':
$query->set( 'meta_key', '_price' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'popularity':
$query->set( 'meta_key', 'total_sales' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'rating':
$query->set( 'meta_key', '_wc_average_rating' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); break;
case 'date':
$query->set( 'orderby', 'date' ); $query->set( 'order', 'DESC' ); break;
case 'menu_order':
case 'default':
default:
$query->set( 'orderby', 'menu_order title' ); $query->set( 'order', 'ASC' ); break;
}
}
}
/** =========================
* AJAX: terms (namespaced)
* ========================= */
if ( ! function_exists( 'al_pf_ajax_terms' ) ) {
function al_pf_ajax_terms() {
if ( empty( $_REQUEST['security'] ) || ! wp_verify_nonce( $_REQUEST['security'], 'alpf_terms' ) ) {
wp_send_json_error( array( 'message' => 'Invalid nonce' ), 403 );
}
$ns = ! empty( $_REQUEST['ns'] ) ? sanitize_key( wp_unslash( $_REQUEST['ns'] ) ) : '';
$brand_tax = '';
$brand_tax_req = alpf_get_param( 'pf_brand_tax', $ns );
if ( ! empty( $brand_tax_req ) ) {
$brand_tax = sanitize_key( wp_unslash( $brand_tax_req ) );
if ( ! taxonomy_exists( $brand_tax ) ) $brand_tax = '';
}
if ( ! $brand_tax ) $brand_tax = al_pf_get_brand_taxonomy();
$attr_tax_slugs = array();
if ( ! empty( $_REQUEST['expose_attrs'] ) ) {
foreach ( explode( ',', (string) wp_unslash( $_REQUEST['expose_attrs'] ) ) as $maybe ) {
$tax = sanitize_key( trim( $maybe ) );
if ( $tax && taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
} else if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
foreach ( wc_get_attribute_taxonomies() as $ga ) {
$tax = 'pa_' . $ga->attribute_name;
if ( taxonomy_exists( $tax ) ) $attr_tax_slugs[] = $tax;
}
}
$ctx_tax = ! empty( $_REQUEST['ctx_tax'] ) ? sanitize_key( wp_unslash( $_REQUEST['ctx_tax'] ) ) : '';
$ctx_terms = array();
if ( ! empty( $_REQUEST['ctx_terms'] ) ) {
$ctx_terms = array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', (string) wp_unslash( $_REQUEST['ctx_terms'] ) ) ) ) );
}
$terms = al_pf_collect_terms_for_filters( $brand_tax, $attr_tax_slugs, array(
'tax' => $ctx_tax,
'terms' => $ctx_terms,
), $ns );
$out = array(
'cats' => array(),
'brands' => array(),
'attrs' => array(),
'attr_labels' => array(),
'brand_tax' => $brand_tax,
);
foreach ( (array) $terms['cats'] as $t ) {
$out['cats'][] = array( 'slug'=>$t->slug, 'name'=>$t->name );
}
foreach ( (array) $terms['brands'] as $b ) {
$out['brands'][] = array(
'slug' => $b->slug,
'name' => $b->name,
'logo' => al_pf_get_term_logo_url( $b->term_id ),
);
}
foreach ( (array) $terms['attrs'] as $tax => $arr ) {
$group = array();
foreach ( (array) $arr as $term ) {
$group[] = array( 'slug'=>$term->slug, 'name'=>$term->name, 'tax'=>$tax );
}
$out['attrs'][ $tax ] = $group;
}
$all_attr_taxes = $attr_tax_slugs ? $attr_tax_slugs : array_keys( (array) $terms['attrs'] );
foreach ( $all_attr_taxes as $tax ) {
$out['attr_labels'][ $tax ] = function_exists('wc_attribute_label') ? wc_attribute_label( $tax ) : $tax;
}
nocache_headers();
wp_send_json_success( $out );
}
}
add_action( 'wp_ajax_nopriv_alpf_terms', 'al_pf_ajax_terms' );
add_action( 'wp_ajax_alpf_terms', 'al_pf_ajax_terms' );
/** =========================
* Plugin defaults / developer hooks
* =========================
* Ensure first-paint sale filtering for the common namespace "sale_query_id".
*/
add_filter( 'alpf_default_source_map', function( $map ){
if ( ! is_array( $map ) ) $map = array();
if ( empty( $map['sale_query_id'] ) ) {
$map['sale_query_id'] = 'sale';
}
return $map;
}, 5 );
/** Cache key extra — language/currency awareness (optional but helpful) */
add_filter( 'alpf_transient_key_extra', function( $extra ) {
$lang = defined( 'ICL_LANGUAGE_CODE' ) ? ICL_LANGUAGE_CODE : ( function_exists( 'pll_current_language' ) ? pll_current_language() : '' );
$currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '';
return trim( $extra . '|' . $lang . '|' . $currency, '|' );
}, 5 );
/** Index + bust term scan caches when catalog changes */
if ( ! function_exists( 'alpf_index_transient' ) ) {
function alpf_index_transient( $key ) {
$idx = get_option( 'alpf_terms_keys', array() );
if ( ! in_array( $key, $idx, true ) ) {
$idx[] = $key;
update_option( 'alpf_terms_keys', $idx, false );
}
}
}
if ( ! function_exists( 'alpf_clear_terms_cache' ) ) {
function alpf_clear_terms_cache() {
$idx = (array) get_option( 'alpf_terms_keys', array() );
foreach ( $idx as $k ) { delete_transient( $k ); }
update_option( 'alpf_terms_keys', array(), false );
delete_transient( 'alpf_sale_ids_v1' );
}
}
add_action( 'save_post_product', 'alpf_clear_terms_cache' );
add_action( 'created_term', 'alpf_clear_terms_cache', 10, 3 );
add_action( 'edited_term', 'alpf_clear_terms_cache', 10, 3 );
add_action( 'delete_term', 'alpf_clear_terms_cache', 10, 4 );
/**
* Plugin Name: WooCommerce – YouTube Video URL Product Field
* Description: Adds a "YouTube Video URL" custom field to WooCommerce products (General tab), and shows a play icon over the product gallery to open the video in a popup.
* Author: Your Name
* Version: 1.1.0
* License: GPL-2.0+
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WC_YouTube_URL_Field {
const META_KEY = '_youtube_video_url';
public function __construct() {
// Admin: field render + save
add_action( 'woocommerce_product_options_general_product_data', [ $this, 'render_field' ] );
add_action( 'woocommerce_admin_process_product_object', [ $this, 'save_field_object' ] );
add_action( 'woocommerce_process_product_meta', [ $this, 'save_field_meta' ] );
// Frontend: enqueue & add play overlay in single product gallery
add_action( 'wp_enqueue_scripts', [ $this, 'maybe_enqueue_frontend_assets' ] );
add_action( 'woocommerce_product_thumbnails', [ $this, 'output_play_icon_overlay' ], 5 );
}
/* ---------------------------
* Admin field (General tab)
* --------------------------*/
public function render_field() {
echo '<div class="options_group">';
woocommerce_wp_text_input( [
'id' => self::META_KEY,
'label' => __( 'YouTube Video URL', 'woocommerce' ),
'placeholder' => 'https://www.youtube.com/watch?v=VIDEO_ID',
'desc_tip' => true,
'description' => __( 'Paste a full YouTube URL (watch, youtu.be, or shorts).', 'woocommerce' ),
'type' => 'url',
] );
echo '</div>';
}
public function save_field_object( $product ) {
if ( ! current_user_can( 'manage_woocommerce' ) ) return;
if ( isset( $_POST[ self::META_KEY ] ) ) {
$url = trim( wp_unslash( $_POST[ self::META_KEY ] ) );
$product->update_meta_data( self::META_KEY, $url ? esc_url_raw( $url ) : '' );
}
}
public function save_field_meta( $post_id ) {
if ( ! current_user_can( 'manage_woocommerce' ) ) return;
if ( isset( $_POST[ self::META_KEY ] ) ) {
$url = trim( wp_unslash( $_POST[ self::META_KEY ] ) );
update_post_meta( $post_id, self::META_KEY, $url ? esc_url_raw( $url ) : '' );
}
}
/* ---------------------------
* Frontend assets & overlay
* --------------------------*/
public function maybe_enqueue_frontend_assets() {
if ( ! is_product() ) return;
$product_id = get_queried_object_id();
if ( ! $product_id ) return;
$url = get_post_meta( $product_id, self::META_KEY, true );
$video_id = $this->parse_youtube_id( $url );
if ( ! $video_id ) return;
// jQuery + Fancybox 3 (CDN)
wp_enqueue_script( 'jquery' );
wp_enqueue_style( 'fancybox-css', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css', [], '3.5.7' );
wp_enqueue_script( 'fancybox-js', 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js', [ 'jquery' ], '3.5.7', true );
// Minimal CSS to position the play icon
$css = '
.wc-yt-trigger{
position:absolute;
top:12px;
left:12px;
z-index:9;
display:inline-flex;
align-items:center;
justify-content:center;
width:64px;height:64px;
border-radius:50%;
background:#f51e46;
opacity:.9;
text-decoration:none;
}
.wc-yt-trigger svg{width:28px;height:28px;fill:#fff}
.wc-yt-trigger:hover{opacity:1}
.woocommerce-product-gallery{position:relative}
';
wp_add_inline_style( 'fancybox-css', $css );
}
public function output_play_icon_overlay() {
if ( ! is_product() ) return;
global $product;
if ( ! $product instanceof WC_Product ) return;
$url = get_post_meta( $product->get_id(), self::META_KEY, true );
$video_id = $this->parse_youtube_id( $url );
if ( ! $video_id ) return;
// Use a regular watch URL; Fancybox 3 understands YT links
$watch = esc_url( "https://www.youtube.com/watch?v={$video_id}" );
// Button overlay (accessible)
echo '<a href="' . $watch . '" class="wc-yt-trigger" data-fancybox data-width="1280" data-height="720" aria-label="' . esc_attr__( 'Play product video', 'woocommerce' ) . '">
<svg viewBox="0 0 448 512" aria-hidden="true" focusable="false"><path d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"/></svg>
</a>';
}
/* ---------------------------
* Helpers
* --------------------------*/
/**
* Extract a YouTube video ID from various URL formats.
* Supports: watch?v=, youtu.be/, shorts/
*
* @param string $url
* @return string|null
*/
private function parse_youtube_id( $url ) {
if ( empty( $url ) || ! is_string( $url ) ) return null;
// Normalize
$url = trim( $url );
// youtu.be/<id>
if ( preg_match( '~(?:https?://)?(?:www\.)?youtu\.be/([a-zA-Z0-9_-]{6,})~', $url, $m ) ) {
return $m[1];
}
// youtube.com/watch?v=<id> or &v=
if ( preg_match( '~[?&]v=([a-zA-Z0-9_-]{6,})~', $url, $m ) ) {
return $m[1];
}
// youtube.com/shorts/<id>
if ( preg_match( '~(?:https?://)?(?:www\.)?youtube\.com/shorts/([a-zA-Z0-9_-]{6,})~', $url, $m ) ) {
return $m[1];
}
return null;
}
}
new WC_YouTube_URL_Field();
// ABFinder: /izzo-kereso/ + /izzo-kereso/* route-ok az Elementor oldalhoz
add_action( 'init', function () {
// base oldal
add_rewrite_rule(
'^izzo-kereso/?$',
'index.php?pagename=izzo-kereso&abf_page=1',
'top'
);
// bármilyen alútvonal (bmw / bmw/x6 / bmw/x6/2019 / stb.)
add_rewrite_rule(
'^izzo-kereso/(.+)/?$',
'index.php?pagename=izzo-kereso&abf_page=1&abf_path=$matches[1]',
'top'
);
}, 1 );
add_filter( 'query_vars', function ( $vars ) {
$vars[] = 'abf_page';
$vars[] = 'abf_path';
return $vars;
} );
// WP canonical ne dobáljon át
add_filter( 'redirect_canonical', function ( $redirect_url ) {
$req = $_SERVER['REQUEST_URI'] ?? '';
if ( strpos( $req, '/izzo-kereso' ) === 0 ) {
return false;
}
return $redirect_url;
}, 10, 1 );
// Rank Math canonical se “javítson”
add_filter( 'rank_math/frontend/canonical', function ( $canonical ) {
$req = $_SERVER['REQUEST_URI'] ?? '';
if ( strpos( $req, '/izzo-kereso' ) === 0 ) {
return home_url( '/izzo-kereso/' );
}
return $canonical;
} );
add_action('admin_init', function () {
if (!current_user_can('manage_options')) return;
if (get_option('abf_flush_done')) return;
flush_rewrite_rules();
update_option('abf_flush_done', 1);
});
add_filter('request', function($q) {
$uri = $_SERVER['REQUEST_URI'] ?? '';
$path = trim(parse_url($uri, PHP_URL_PATH) ?? '', '/');
// csak a pontos base route-ra
if ($path === 'izzo-kereso') {
// kényszerítjük oldalnak
$q = [
'pagename' => 'izzo-kereso',
'abf_page' => 1,
];
}
return $q;
});
add_filter('query_vars', function ($vars) {
$vars[] = 'abf_page';
$vars[] = 'abf_path';
return $vars;
});
add_filter('vp_woo_pont_db_import_foxpost', function($points){
$filtered = array();
foreach ($points as $point) {
if(strpos($point['name'], 'Z-BOX') !== 0) {
$filtered[] = $point;
}
}
return $filtered;
});