first commit

This commit is contained in:
2026-04-28 15:13:50 +02:00
commit a95acc355b
63745 changed files with 9487948 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery(document).ready(function($) {
/**
* Handle the sync products button click event
*
* @since 3.5.0
*
* @param {object} event
*/
$('#wc-facebook-enhanced-settings-sync-products').click(function(event) {
event.preventDefault();
var button = $(this);
button.html('Syncing...');
button.prop('disabled', true);
var data = {
action: "wc_facebook_sync_products",
nonce: wc_facebook_enhanced_settings_sync.sync_products_nonce
};
$.post(wc_facebook_enhanced_settings_sync.ajax_url, data, function(response) {
if (response.success) {
button.html('Sync completed');
button.prop('disabled', false);
} else {
button.html('Sync failed');
button.prop('disabled', false);
}
}).fail(function() {
button.html('Sync failed');
button.prop('disabled', false);
});
});
/**
* Handle the sync coupons button click event
*
* @since 3.5.0
*
* @param {object} event
*/
$('#wc-facebook-enhanced-settings-sync-coupons').click(function(event) {
event.preventDefault();
var button = $(this);
button.html('Syncing...');
button.prop('disabled', true);
var data = {
action: "wc_facebook_sync_coupons",
nonce: wc_facebook_enhanced_settings_sync.sync_coupons_nonce
};
$.post(wc_facebook_enhanced_settings_sync.ajax_url, data, function(response) {
if (response.success) {
button.html('Sync completed');
button.prop('disabled', false);
} else {
button.html('Sync failed');
button.prop('disabled', false);
}
}).fail(function() {
button.html('Sync failed');
button.prop('disabled', false);
});
});
/**
* Handle the sync shipping profiles button click event
*
* @since 3.5.0
*
* @param {object} event
*/
$('#wc-facebook-enhanced-settings-sync-shipping-profiles').click(function(event) {
event.preventDefault();
var button = $(this);
button.html('Syncing...');
button.prop('disabled', true);
var data = {
action: "wc_facebook_sync_shipping_profiles",
nonce: wc_facebook_enhanced_settings_sync.sync_shipping_profiles_nonce
};
$.post(wc_facebook_enhanced_settings_sync.ajax_url, data, function(response) {
if (response.success) {
button.html('Sync completed');
button.prop('disabled', false);
} else {
button.html('Sync failed');
button.prop('disabled', false);
}
}).fail(function() {
button.html('Sync failed');
button.prop('disabled', false);
});
});
/**
* Handle the sync navigation menu button click event
*
* @since 3.5.0
*
* @param {object} event
*/
$('#wc-facebook-enhanced-settings-sync-navigation-menu').click(function(event) {
event.preventDefault();
var button = $(this);
button.html('Syncing...');
button.prop('disabled', true);
var data = {
action: "wc_facebook_sync_navigation_menu",
nonce: wc_facebook_enhanced_settings_sync.sync_navigation_menu_nonce
};
$.post(wc_facebook_enhanced_settings_sync.ajax_url, data, function(response) {
if (response.success) {
button.html('Sync completed');
button.prop('disabled', false);
} else {
button.html('Sync failed');
button.prop('disabled', false);
}
}).fail(function() {
button.html('Sync failed');
button.prop('disabled', false);
});
});
});

View File

@@ -0,0 +1,342 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery( document ).ready( ( $ ) => {
'use strict';
/**
* Google product category field handler.
*
* @since 2.1.0
*
* @type {WC_Facebook_Google_Product_Category_Fields} object
*/
window.WC_Facebook_Google_Product_Category_Fields = class WC_Facebook_Google_Product_Category_Fields {
/**
* Handler constructor.
*
* @since 2.1.0
*
* @param {Object[]} categories The full categories list, indexed by the category ID
* @param {string} categories[].label The category label
* @param {string[]} categories[].options The category's child categories' IDs
* @param {string} categories[].parent The category's parent category ID
* @param {string} input_id The element that should receive the latest concrete category ID
*/
constructor(categories, input_id) {
this.categories = categories;
this.input_id = input_id;
var $input = $( '#' + this.input_id );
$( '<div id="wc-facebook-google-product-category-fields"></div>' )
.insertBefore( $input )
.on( 'change', 'select.wc-facebook-google-product-category-select', ( event ) => {
this.onChange( $( event.target ) );
} );
this.addInitialSelects( $input.val() );
var optionalSelectorID = this.globalsHolder().enhanced_attribute_optional_selector;
if(typeof(optionalSelectorID) !== 'undefined') {
// Initial trigger for the optional attributes selector
$( '#' + optionalSelectorID ).on('change', function(){
$('.wc-facebook-enhanced-catalog-attribute-optional-row')
.toggleClass('hidden', !$(this).prop("checked"));
});
}
}
globalsHolder() {
if(typeof(facebook_for_woocommerce_product_categories) !== 'undefined'){
return facebook_for_woocommerce_product_categories;
} else if(typeof(facebook_for_woocommerce_settings_sync) !== 'undefined'){
return facebook_for_woocommerce_settings_sync;
} else {
return facebook_for_woocommerce_products_admin;
}
}
getPageType(){
if(typeof(facebook_for_woocommerce_product_categories) !== 'undefined'){
if( $( 'input[name=tag_ID]' ).length === 0){
return this.globalsHolder().enhanced_attribute_page_type_add_category;
} else {
return this.globalsHolder().enhanced_attribute_page_type_edit_category;
}
} else {
return this.globalsHolder().enhanced_attribute_page_type_edit_product;
}
}
/**
* Adds the initial select fields for the previously selected values.
*
* If there is no previously selected value, it adds two selected fields with no selected option.
*
* @param {string} categoryId the selected google product category
*/
addInitialSelects( categoryId ) {
if ( categoryId ) {
this.getSelectedCategoryIds( categoryId ).forEach( ( pair ) => {
this.addSelect( this.getOptions( pair[1] ), pair[0] );
} );
var options = this.getOptions( categoryId );
if ( Object.keys( options ).length ) {
this.addSelect( options );
}
} else {
this.addSelect( this.getOptions() );
this.addSelect( {} );
}
}
/**
* Sets the enhanced attributes to show
*
*/
requestAttributesIfValid() {
// if an input with this id isn't available then we can't show
// enhanced attributes on this page, (for example it may be the
// product sync page)
var canShowEnhancedAttributesID = 'wc_facebook_can_show_enhanced_catalog_attributes_id';
if($( '#'+canShowEnhancedAttributesID ).val() !== 'true'){
return;
}
$('.wc-facebook-enhanced-catalog-attribute-row').remove();
if(this.isValid()) {
var inputSelector = '#' + this.input_id;
var $inputParent = $( inputSelector ).parents('div.form-field');
var optionalSelectorID = this.globalsHolder().enhanced_attribute_optional_selector;
if( this.getPageType() === this.globalsHolder().enhanced_attribute_page_type_edit_category ){
$inputParent = $( inputSelector ).parents('tr.form-field');
} else if( this.getPageType() === this.globalsHolder().enhanced_attribute_page_type_edit_product ) {
$inputParent = $( inputSelector ).parents('p.form-field');
}
$.get( this.globalsHolder().ajax_url, {
action: 'wc_facebook_enhanced_catalog_attributes',
security: '',
selected_category: $( inputSelector ).val(),
tag_id: parseInt($( 'input[name=tag_ID]' ).val(), 10),
taxonomy: $( 'input[name=taxonomy]' ).val(),
item_id: parseInt( $( 'input[name=post_ID]' ).val(), 10 ),
page_type: this.getPageType(),
}, function( response ) {
var $response = $(response);
$( '#' + optionalSelectorID, $response ).on('change', function(){
$('.wc-facebook-enhanced-catalog-attribute-optional-row')
.toggleClass('hidden', !$(this).prop("checked"));
});
$response.insertAfter($inputParent);
// Ensure tooltips work:
$(document.body).trigger('init_tooltips');
});
}
}
/**
* Updates the subsequent selects whenever one of the selects changes.
*
* @since 2.1.0
*/
onChange(element) {
// remove following select fields if their options depended on the value of the current select field
if ( element.hasClass( 'locked' ) ) {
element.closest( '.wc-facebook-google-product-category-field' ).nextAll().remove();
}
var categoryId = element.val();
if ( categoryId ) {
var options = this.getOptions( categoryId );
if ( Object.keys( options ).length ) {
this.addSelect( options );
}
} else {
// use category ID from the last select field that has a selected value
categoryId = element.closest( '#wc-facebook-google-product-category-fields' )
.find( '.wc-facebook-google-product-category-select' )
.not( element )
.last()
.val();
if ( ! categoryId ) {
this.addSelect( {} );
}
}
$( '#' + this.input_id ).val( categoryId );
this.requestAttributesIfValid();
}
/**
* Returns true if there have been at least two levels of category selected
*
* @return {boolean}
*/
isValid() {
var selectsWithValueCount = $('.wc-facebook-google-product-category-select')
.filter(function(_i, el) { return $(el).val() !== ""; })
.length;
return selectsWithValueCount >= 2;
}
/**
* Adds a new select with the given options.
*
* @since 2.1.0
*
* @param {Object.<string, string>} options an object with option IDs as keys and option labels as values
* @param {string} selected the selected option ID
*/
addSelect( options, selected ) {
var $container = $( '#wc-facebook-google-product-category-fields' );
var $otherSelects = $container.find( '.wc-facebook-google-product-category-select' );
var $select = $( '<select class="wc-facebook-google-product-category-select"></select>' );
$otherSelects.addClass( 'locked' );
$container.append( $( '<div class="wc-facebook-google-product-category-field" style="margin-bottom: 16px">' ).append( $select ) );
$select.attr( 'data-placeholder', this.getSelectPlaceholder( $otherSelects, options ) ).append( $( '<option value=""></option>' ) );
Object.keys( options ).forEach( ( key ) => {
$select.append( $( '<option value="' + key + '">' + options[ key ] + '</option>' ) );
} );
$select.val( selected ).select2( { allowClear: true } );
}
/**
* Gets the placeholder string for a select field based on the number of existing select fields.
*
* @since 2.1.0
*
* @param {jQuery} $otherSelects a jQuery object matching existing select fields
* @param {Object.<string, string>} options an object with option IDs as keys and option labels as values
* @return {string}
*/
getSelectPlaceholder( $otherSelects, options ) {
if ( 0 === $otherSelects.length ) {
return facebook_for_woocommerce_google_product_category.i18n.top_level_dropdown_placeholder;
}
if ( 1 === $otherSelects.length && 0 === Object.keys( options ).length ) {
return facebook_for_woocommerce_google_product_category.i18n.second_level_empty_dropdown_placeholder;
}
return facebook_for_woocommerce_google_product_category.i18n.general_dropdown_placeholder;
}
/**
* Gets an array of options for the given category ID.
*
* @since 2.1.0
*
* @param {string} category_id The given category ID
* @return {Object.<string, string>} an object with option IDs as keys and option labels as values
*/
getOptions(category_id) {
if ( 'undefined' === typeof category_id || '' === category_id ) {
return this.getTopLevelOptions();
}
if ( 'undefined' === typeof this.categories[ category_id ] ) {
return [];
}
if ( 'undefined' === typeof this.categories[ category_id ]['options'] ) {
return [];
}
return this.categories[ category_id ]['options'];
}
/**
* Gets an array of top level category options.
*
* @since 2.1.0
*
* @return {Object.<string, string>} an object with option IDs as keys and option labels as values
*/
getTopLevelOptions() {
let options = {};
Object.keys( this.categories ).forEach( ( key ) => {
if ( this.categories[ key ].parent ) {
return;
}
options[ key ] = this.categories[ key ].label;
} );
return options;
}
/**
* Gets the ID of the selected category and all its ancestors.
*
* The method returns an array of arrays, where each entry is a pair of category IDs.
* The first entry in the pair is the category ID and the second entry is the ID of the corresponding parent category.
*
* We use an array of arrays to be able to present the select fields in the correct order.
* Object keys are automatically ordered causing options for categories with larger IDs to be displayed last.
*
* @param {string} categoryId
* @param {Array.<string[]>} categoryId
*/
getSelectedCategoryIds( categoryId ) {
var options = [];
do {
if ( 'undefined' !== typeof this.categories[ categoryId ] ) {
options.push( [ categoryId, this.categories[ categoryId ].parent ] );
categoryId = this.categories[ categoryId ].parent;
}
} while ( '' !== categoryId );
return options.reverse();
}
}
} );

View File

@@ -0,0 +1 @@
// Add new any JS here

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
/*
* Ajax helper function.
* Takes optional payload for POST and optional callback.
*/
function ajax(action, payload = null, callback = null, failcallback = null) {
var data = Object.assign( {}, {
'action': action,
}, payload );
// Since Wordpress 2.8 ajaxurl is always defined in admin header and
// points to admin-ajax.php
jQuery.post(
ajaxurl,
data,
function(response) {
if (callback) {
callback( response );
}
}
).fail(
function(errorResponse){
if (failcallback) {
failcallback( errorResponse );
}
}
);
}
window.fb_woo_infobanner_post_click = function (){
console.log( "Woo infobanner post tip click!" );
return ajax(
'ajax_woo_infobanner_post_click',
{
"_ajax_nonce": wc_facebook_infobanner_jsx.nonce
},
);
};
window.fb_woo_infobanner_post_xout = function() {
console.log( "Woo infobanner post tip xout!" );
return ajax(
'ajax_woo_infobanner_post_xout',
{
"_ajax_nonce": wc_facebook_infobanner_jsx.nonce
},
);
};

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
/*
* Ajax helper function.
* Takes optional payload for POST and optional callback.
*/
function ajax(action, payload = null, cb = null, failcb = null) {
var data = Object.assign( {}, {
'action': action,
}, payload);
// Since Wordpress 2.8 ajaxurl is always defined in admin header and
// points to admin-ajax.php
jQuery.post(
ajaxurl,
data,
function(response) {
if (cb) {
cb( response );
}
}
).fail(
function(errorResponse){
if (failcb) {
failcb( errorResponse );
}
}
);
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
(function( $ ) {
/**
* Determines if the current modal is blocked.
*
* @returns {boolean}
*/
window.isModalBlocked = function() {
let $modal = $( '.wc-backbone-modal-content' );
return $modal.is( '.processing' ) || $modal.parents( '.processing' ).length;
}
/**
* Blocks the current modal.
*/
window.blockModal = function() {
if ( ! isModalBlocked() ) {
return $( '.wc-backbone-modal-content' ).addClass( 'processing' ).block( {
message: null,
overlayCSS: {
background: '#fff',
opacity: 0.6
}
} );
}
}
/**
* Unblocks the current modal.
*/
window.unBlockModal = function() {
$( '.wc-backbone-modal-content' ).removeClass( 'processing' ).unblock();
}
/**
* Closes the current modal.
*/
window.closeExistingModal = function() {
$( '#wc-backbone-modal-dialog .modal-close' ).trigger( 'click' );
}
})( jQuery );

View File

@@ -0,0 +1,126 @@
/**
* Meta for WooCommerce API
*
* @package MetaCommerce
*/
/* global jQuery, fb_api_data */
(function ($) {
'use strict';
/**
* Meta WooCommerce API Client Factory
* Creates an API client with the provided nonce
*
* NONCE is a required argument for all requests! Each calling page must provide a fresh nonce;
* this ensures that other scripts on the page cannot make unauthenticated requests to the API.
*
* @param {string} nonce The WordPress REST API nonce
* @return {Object} API client instance
*/
window.GeneratePluginAPIClient = function (nonce) {
// Create a new API client instance
const apiClient = {
// Base request method
request: function (endpoint, method, data) {
return new Promise(function (resolve, reject) {
$.ajax({
url: fb_api_data.api_url + endpoint,
method: method,
data: JSON.stringify(data),
contentType: 'application/json',
beforeSend: function (xhr) {
xhr.setRequestHeader('X-WP-Nonce', nonce);
},
success: function (response) {
resolve(response);
},
error: function (jqXHR, textStatus, errorThrown) {
reject({
status: jqXHR.status,
message: errorThrown
});
}
});
});
},
// Validation helper
validateParam: function (value, type) {
switch (type) {
case 'string':
return typeof value === 'string';
case 'int':
case 'integer':
return Number.isInteger(value);
case 'float':
case 'double':
return typeof value === 'number';
case 'bool':
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return typeof value === 'object' && value !== null && !Array.isArray(value);
default:
return true; // Unknown type, assume valid
}
}
};
// Generate API methods from endpoint definitions
if (fb_api_data.endpoints && typeof fb_api_data.endpoints === 'object') {
// Convert object to array if needed
const endpointsArray = Array.isArray(fb_api_data.endpoints)
? fb_api_data.endpoints
: Object.values(fb_api_data.endpoints);
endpointsArray.forEach(function (endpoint) {
// Create method name from class name
var methodName = endpoint.className.replace('Request', '');
methodName = methodName.charAt(0).toLowerCase() + methodName.slice(1);
// Create the method with validation
apiClient[methodName] = function (data) {
// Initialize data if not provided
data = data || {};
// Validate required parameters
if (endpoint.required && Array.isArray(endpoint.required)) {
for (var i = 0; i < endpoint.required.length; i++) {
var param = endpoint.required[i];
if (data[param] === undefined) {
return Promise.reject({
status: 400,
message: 'Missing required parameter: ' + param
});
}
}
}
// Validate parameter types
if (endpoint.params && typeof data === 'object') {
for (var key in data) {
if (endpoint.params[key]) {
if (!apiClient.validateParam(data[key], endpoint.params[key])) {
return Promise.reject({
status: 400,
message: 'Invalid type for parameter ' + key + '. Expected ' + endpoint.params[key]
});
}
}
}
}
// Make the request
return apiClient.request(endpoint.path, endpoint.method, data);
};
});
}
return apiClient;
};
})(jQuery);

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery( document ).ready( function( $ ) {
//Setting up opt out modal
let modal;
$(document).on('click', '#modal_opt_out_button', function(e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_facebook_opt_out_of_sync',
nonce: facebook_for_woocommerce_plugin_update.opt_out_of_sync,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
$('#opt_out_banner').hide();
$('#opted_our_successfullly_banner').show();
modal.remove();
}
}).fail(function(xhr) {
modal.remove();
});
});
/**
* Banner dismissed callback
*/
$(document).on('click','#opt_out_banner .notice-dismiss, #opted_our_successfullly_banner .notice-dismiss', function (e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_banner_close_action',
nonce: facebook_for_woocommerce_plugin_update.banner_close,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
modal.remove();
});
});
/**
* Banner dismissed callback
*/
$(document).on('click','.plugin_updated_successfully .notice-dismiss', function (e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_banner_post_update_close_action',
nonce: facebook_for_woocommerce_plugin_update.banner_close,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
modal.remove();
});
});
/**
* Banner dismissed callback
* but when master sync is off
*/
$(document).on('click','#plugin_updated_successfully_but_master_sync_off .notice-dismiss', function (e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_banner_post_update__master_sync_off_close_action',
nonce: facebook_for_woocommerce_plugin_update.banner_close,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
modal.remove();
});
});
/**
* Banner dismissed callback
*/
$(document).on('click','.plugin_updated_successfully .notice-dismiss', function (e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_banner_post_update_close_action',
nonce: facebook_for_woocommerce_plugin_update.banner_close,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
modal.remove();
});
});
/**
* Banner dismissed callback
* but when master sync is off
*/
$(document).on('click','#plugin_updated_successfully_but_master_sync_off .notice-dismiss', function (e) {
e.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_banner_post_update__master_sync_off_close_action',
nonce: facebook_for_woocommerce_plugin_update.banner_close,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
modal.remove();
});
});
// Opt out sync controls
$('.opt_out_of_sync_button').on('click', function(event) {
event.preventDefault();
modal = new $.WCBackboneModal.View({
target: 'facebook-for-woocommerce-modal',
string: {
message: facebook_for_woocommerce_plugin_update.opt_out_confirmation_message,
buttons: facebook_for_woocommerce_plugin_update.opt_out_confirmation_buttons
}
});
})
// Sync all products
$('#sync_all_products').on('click',function(event) {
event.preventDefault();
let context = $(this);
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_facebook_sync_all_products',
nonce: facebook_for_woocommerce_plugin_update.sync_back_in,
} ,function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if( data.success ) {
$('#plugin_updated_successfully_but_master_sync_off').hide();
$('#plugin_updated_successfully_after_user_opts_in').show();
}
}).fail(function(xhr) {
// No fail conditon
});
});
// Sync all products
$('#sync_all_products').on('click',function(event) {
event.preventDefault();
let context = $(this);
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_facebook_sync_all_products',
nonce: facebook_for_woocommerce_plugin_update.sync_back_in,
} ,function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if( data.success ) {
$('#plugin_updated_successfully_but_master_sync_off').hide();
$('#plugin_updated_successfully_after_user_opts_in').show();
}
}).fail(function(xhr) {
// No fail condition
});
});
// Product set banner dismissed callback
$('.fb-product-set-banner').on('click', '.notice-dismiss', function(event) {
event.preventDefault();
$.post( facebook_for_woocommerce_plugin_update.ajax_url, {
action: 'wc_facebook_product_set_banner_closed',
nonce: facebook_for_woocommerce_plugin_update.product_set_banner_closed_nonce,
}, function (response){
data = typeof response === "string" ? JSON.parse(response) : response;
if(data.success){
// No success condition
}
}).fail(function(xhr) {
// No fail conditon
});
});
});

View File

@@ -0,0 +1,95 @@
/**
* Product Attributes Settings Page Scripts
*
* @package facebook-for-woocommerce
*/
jQuery(document).ready(function($) {
// Fix duplicated content issue
var $infoNotes = $('.wc-facebook-info-note');
if ($infoNotes.length > 1) {
$infoNotes.not(':first').closest('tr').remove();
}
// Fix duplicated mapping tables
var $mappingRows = $('th.titledesc:contains("WooCommerce to Facebook Field Mapping")').closest('tr');
if ($mappingRows.length > 1) {
$mappingRows.not(':first').remove();
}
// Initialize select2 for attribute and field dropdowns
function initializeSelects() {
if ($.fn.select2) {
$('.wc-attribute-search, .fb-field-search').select2({
width: '100%',
placeholder: function() {
return $(this).data('placeholder');
}
});
}
}
// Initialize on page load
initializeSelects();
// Fix form field names when a mapping row changes
$('#facebook-attribute-mapping-table').on('change', '.wc-attribute-search', function() {
var $select = $(this);
var $row = $select.closest('tr');
var attribute = $select.val();
// Update the name attributes of other fields in the row
if (attribute) {
$row.find('.fb-field-search').attr('name', 'wc_facebook_field_mapping[' + attribute + ']');
$row.find('.fb-default-value').attr('name', 'wc_facebook_attribute_default[' + attribute + ']');
} else {
// If no attribute is selected, use an empty name to prevent conflicts
$row.find('.fb-field-search').attr('name', 'wc_facebook_field_mapping[]');
$row.find('.fb-default-value').attr('name', 'wc_facebook_attribute_default[]');
}
});
// Add new mapping row in edit mode
$('.add-new-mapping').on('click', function() {
var $lastRow = $('#facebook-attribute-mapping-table tbody tr:last-child');
var $newRow = $lastRow.clone();
// Clear values
$newRow.find('select').val('').trigger('change');
$newRow.find('input[type="text"]').val('');
$newRow.find('input[type="hidden"]').remove();
// Reinitialize select2
if ($.fn.select2) {
$newRow.find('select').select2('destroy');
}
// Append to table
$('#facebook-attribute-mapping-table tbody').append($newRow);
// Reinitialize select2 for the new row
initializeSelects();
});
// Add new mapping from view mode - redirects to edit mode with a new empty row
$('.add-mapping-button').on('click', function(e) {
// Use the href attribute directly, which has been set with all necessary parameters
window.location.href = $(this).attr('href');
e.preventDefault();
});
// Remove mapping row
$('#facebook-attribute-mapping-table').on('click', '.remove-mapping-row', function(e) {
e.preventDefault();
// Don't remove if it's the only row
if ($('#facebook-attribute-mapping-table tbody tr').length > 1) {
$(this).closest('tr').remove();
} else {
// Clear values instead
$(this).closest('tr').find('select').val('').trigger('change');
$(this).closest('tr').find('input[type="text"]').val('');
$(this).closest('tr').find('input[type="hidden"]').remove();
}
});
});

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery( document ).ready( ( $ ) => {
let $form = $( 'form[id="edittag"]' );
let $defaultCategoryField = $( '#wc_facebook_google_product_category_id' );
let originalDefaultCategoryId = $defaultCategoryField.val();
$form.on( 'submit', function( event ) {
if ( $form.data( 'allow-submit' ) || $defaultCategoryField.val() === originalDefaultCategoryId ) {
return;
}
event.preventDefault();
$( '#wc-backbone-modal-dialog .modal-close' ).trigger( 'click' );
new $.WCBackboneModal.View( {
target: 'facebook-for-woocommerce-modal',
string: {
message: facebook_for_woocommerce_product_categories.default_google_product_category_modal_message,
buttons: facebook_for_woocommerce_product_categories.default_google_product_category_modal_buttons
}
} );
$( document.body )
.off( 'wc_backbone_modal_response.facebook_for_commerce' )
.on( 'wc_backbone_modal_response.facebook_for_commerce', function() {
$form.data( 'allow-submit', true ).find( ':submit' ).trigger( 'click' );
} );
} );
// Add new category button handler to clear Google Product Category selections when clicked
$('#submit').on('click', function(e) {
// Only proceed if this is the "Add new category" button
if ($(this).val() === 'Add new category') {
// Check if the Google Product Category Fields handler exists
if (typeof window.wc_facebook_google_product_category_fields !== 'undefined') {
// Clear selections
$('#' + window.wc_facebook_google_product_category_fields.input_id).val('');
$('#wc-facebook-google-product-category-fields').empty();
$('.wc-facebook-enhanced-catalog-attribute-row').remove();
// Recreate initial empty selectors
window.wc_facebook_google_product_category_fields.addInitialSelects('');
// Set the last dropdown's margin bottom
$('.wc-facebook-google-product-category-field').last().attr('style', 'margin-bottom: 24px');
}
}
});
} );

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery( document ).ready( function( $ ) {
let $form = $( 'form.wc-facebook-settings' );
let $defaultCategoryField = $( '#wc_facebook_google_product_category_id' );
let defaultCategoryId = $defaultCategoryField.val();
$form.on( 'submit', function( event ) {
if ( $form.data( 'allow-submit' ) || $defaultCategoryField.val() === defaultCategoryId ) {
return;
}
event.preventDefault();
$( '#wc-backbone-modal-dialog .modal-close' ).trigger( 'click' );
new $.WCBackboneModal.View( {
target: 'facebook-for-woocommerce-modal',
string: {
message: $defaultCategoryField.val() ? facebook_for_woocommerce_settings_commerce.default_google_product_category_modal_message : facebook_for_woocommerce_settings_commerce.default_google_product_category_modal_message_empty,
buttons: facebook_for_woocommerce_settings_commerce.default_google_product_category_modal_buttons
}
} );
$( document.body )
.off( 'wc_backbone_modal_response.facebook_for_commerce' )
.on( 'wc_backbone_modal_response.facebook_for_commerce', function() {
$form.data( 'allow-submit', true ).find( ':submit' ).trigger( 'click' );
} );
} );
$( '.woocommerce-help-tip' ).tipTip( {
'attribute': 'data-tip',
'fadeIn': 50,
'fadeOut': 50,
'delay': 200
} );
} );

View File

@@ -0,0 +1,308 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
jQuery( document ).ready( function( $ ) {
/**
* Gets any new excluded categories being added.
*
* @return {string[]}
*/
function getExcludedCategoriesAdded() {
const newCategoryIDs = $( '#wc_facebook_excluded_product_category_ids' ).val();
let oldCategoryIDs = [];
if ( window.facebook_for_woocommerce_settings_sync && window.facebook_for_woocommerce_settings_sync.excluded_category_ids ) {
oldCategoryIDs = window.facebook_for_woocommerce_settings_sync.excluded_category_ids;
}
// return IDs that are in the new value that were not in the saved value
return $( newCategoryIDs ).not( oldCategoryIDs ).get();
}
/**
* Gets any new excluded tags being added.
*
* @return {string[]}
*/
function getExcludedTagsAdded() {
const newTagIDs = $( '#wc_facebook_excluded_product_tag_ids' ).val();
let oldTagIDs = [];
if ( window.facebook_for_woocommerce_settings_sync && window.facebook_for_woocommerce_settings_sync.excluded_tag_ids ) {
oldTagIDs = window.facebook_for_woocommerce_settings_sync.excluded_tag_ids;
}
// return IDs that are in the new value that were not in the saved value
return $( newTagIDs ).not( oldTagIDs ).get();
}
/**
* Toggles availability of input in setting groups.
*
* @param {boolean} enable whether fields in this group should be enabled or not
*/
function toggleSettingOptions( enable ) {
$( '.product-sync-field' ).each( function() {
let $element = $( this );
if ( $( this ).hasClass( 'wc-enhanced-select' ) ) {
$element = $( this ).next( 'span.select2-container' );
}
if ( enable ) {
$element.css( 'pointer-events', 'all' ).css( 'opacity', '1.0' );
} else {
$element.css( 'pointer-events', 'none' ).css( 'opacity', '0.4' );
}
} );
}
$( '.woocommerce-help-tip' ).tipTip( {
'attribute': 'data-tip',
'fadeIn': 50,
'fadeOut': 50,
'delay': 200
} );
if ( $( 'form.wc-facebook-settings' ).hasClass( 'disconnected' ) ) {
toggleSettingOptions( false );
}
// toggle availability of options within field groups
$( 'input#wc_facebook_enable_product_sync' ).on( 'change', function ( e ) {
if ( $( 'form.wc-facebook-settings' ).hasClass( 'disconnected' ) ) {
$( this ).css( 'pointer-events', 'none' ).css( 'opacity', '0.4' );
return;
}
toggleSettingOptions( $( this ).is( ':checked' ) );
} ).trigger( 'change' );
let submitSettingsSave = false;
$( 'input[name="save_product_sync_settings"]' ).on( 'click', function ( e ) {
if ( ! submitSettingsSave ) {
e.preventDefault();
} else {
return true;
}
const $submitButton = $( this ),
categoriesAdded = getExcludedCategoriesAdded(),
tagsAdded = getExcludedTagsAdded();
if ( categoriesAdded.length > 0 || tagsAdded.length > 0 ) {
$.post( facebook_for_woocommerce_settings_sync.ajax_url, {
action: 'facebook_for_woocommerce_set_excluded_terms_prompt',
security: facebook_for_woocommerce_settings_sync.set_excluded_terms_prompt_nonce,
categories: categoriesAdded,
tags: tagsAdded,
}, function ( response ) {
if ( response && ! response.success ) {
// close existing modals
$( '#wc-backbone-modal-dialog .modal-close' ).trigger( 'click' );
// open new modal, populate template with AJAX response data
new $.WCBackboneModal.View( {
target: 'facebook-for-woocommerce-modal',
string: response.data,
} );
// exclude products: submit form as normal
$( '.facebook-for-woocommerce-confirm-settings-change' ).on( 'click', function () {
blockModal();
submitSettingsSave = true;
$submitButton.trigger( 'click' );
} );
} else {
// no modal displayed: submit form as normal
submitSettingsSave = true;
$submitButton.trigger( 'click' );
}
} );
} else {
// no terms added: submit form as normal
submitSettingsSave = true;
$submitButton.trigger( 'click' );
}
} );
// mark as in-progress if syncing when the page is loaded
if ( facebook_for_woocommerce_settings_sync.sync_in_progress ) {
syncInProgress();
}
// handle the sync button click
$( '#woocommerce-facebook-settings-sync-products' ).click( function( event ) {
event.preventDefault();
if ( confirm( facebook_for_woocommerce_settings_sync.i18n.confirm_sync ) ) {
setProductSyncStatus();
let startTime = Date.now();
$.post( facebook_for_woocommerce_settings_sync.ajax_url, {
action: 'wc_facebook_sync_products',
nonce: facebook_for_woocommerce_settings_sync.sync_products_nonce,
}, function ( response ) {
if ( ! response.success ) {
let error = facebook_for_woocommerce_settings_sync.i18n.general_error;
if ( response.data && response.data.length > 0 ) {
error = response.data;
}
clearSyncInProgress( error );
} else {
// get the current sync status after a successful response but make sure to wait at least 10 seconds since the button was pressed
setTimeout( getSyncStatus, Math.max( 0, 10000 - ( Date.now() - startTime ) ) );
}
} ).fail( function() {
clearSyncInProgress( facebook_for_woocommerce_settings_sync.i18n.general_error );
} );
}
} );
/**
* Sets the UI as sync in progress and starts an interval to check the background sync status.
*
* @since 2.0.0
*
* @param count number of items remaining
*/
function syncInProgress( count = null ) {
setProductSyncStatus( count );
if ( ! window.syncStatusInterval ) {
window.syncStatusInterval = setInterval( getSyncStatus, 10000 );
}
}
/**
* Sets the UI as sync in progress.
*
* @since 2.0.0
*
* @param count number of items remaining
*/
function setProductSyncStatus( count = null ) {
toggleSettingOptions( false );
$( 'input#wc_facebook_enable_product_sync, input[name="save_product_sync_settings"]' ).css( 'pointer-events', 'none' ).css( 'opacity', '0.4' );
let message = facebook_for_woocommerce_settings_sync.i18n.sync_in_progress;
if ( count ) {
if ( count > 1 ) {
message = message + facebook_for_woocommerce_settings_sync.i18n.sync_remaining_items_plural;
} else {
message = message + facebook_for_woocommerce_settings_sync.i18n.sync_remaining_items_singular
}
message = message.replace( '{count}', count );
}
// set products sync status
$( '#sync_progress' ).show().html( message ).css( 'color', 'inherit' );
facebook_for_woocommerce_settings_sync.sync_in_progress = true;
}
/**
* Clears any UI for sync in progress.
*
* @since 2.0.0
*
* @param error message to display
*/
function clearSyncInProgress( error = '' ) {
facebook_for_woocommerce_settings_sync.sync_in_progress = false;
clearInterval( window.syncStatusInterval );
window.syncStatusInterval = null;
toggleSettingOptions( true );
$( 'input#wc_facebook_enable_product_sync, input[name="save_product_sync_settings"]' ).css( 'pointer-events', 'all' ).css( 'opacity', '1' );
if ( error ) {
$( '#sync_progress' ).show().html( error ).css( 'color', '#DC3232' );
} else {
$( '#sync_progress' ).hide();
}
}
/**
* Gets the current sync status.
*
* @since 2.0.0
*/
function getSyncStatus() {
if ( ! facebook_for_woocommerce_settings_sync.sync_in_progress ) {
return;
}
$.post( facebook_for_woocommerce_settings_sync.ajax_url, {
action: 'wc_facebook_get_sync_status',
nonce: facebook_for_woocommerce_settings_sync.sync_status_nonce,
}, function ( response ) {
console.log( response );
if ( response.success ) {
// the returned data represents the number of products remaining
if ( response.data > 0 ) {
syncInProgress( response.data );
} else {
clearSyncInProgress();
}
}
} );
}
} );

View File

@@ -0,0 +1,13 @@
jQuery(function ($) {
$(document).on('click', '.fb-wa-banner .wa-close-button', function (e) {
e.preventDefault();
$.post(WCFBAdminBanner.ajax_url, {
action: 'wc_facebook_dismiss_banner',
nonce: WCFBAdminBanner.nonce,
banner_id: WCFBAdminBanner.banner_id
}).done(function (response) {
$('.fb-wa-banner').remove();
});
});
});

View File

@@ -0,0 +1,9 @@
jQuery(function ($) {
$(document).on('click', '.wc-facebook-global-notice.is-dismissible .notice-dismiss', function () {
$.post(WCFBAdminNotice.ajax_url, {
action: 'wc_facebook_dismiss_notice',
nonce: WCFBAdminNotice.nonce,
notice_id: WCFBAdminNotice.notice_id
});
});
});

View File

@@ -0,0 +1,241 @@
/**
* Facebook Pixel Events - External JavaScript Handler
*
* This script fires pixel events in an isolated execution context,
* ensuring events are sent even if other plugins cause JavaScript errors.
*
* Supports WooCommerce Blocks via Store API fetch interception.
* The pixel event data is read from the Store API response extensions.
*
* @package FacebookCommerce
*/
(function() {
'use strict';
// Early exit if no data from PHP
if (typeof wc_facebook_pixel_data === 'undefined') {
return;
}
var data = wc_facebook_pixel_data;
var firedEvents = {};
/**
* Build event data object for fbq()
*
* @param {Object} event Event object from PHP
* @return {Object} Prepared event data
*/
function buildEventData(event) {
return {
method: event.method || 'track',
name: event.name,
params: event.params || {},
eventId: event.eventId || null
};
}
/**
* Check if event should be skipped (already fired)
*
* @param {string|null} eventId Event ID for deduplication
* @return {boolean} True if should skip
*/
function shouldSkipEvent(eventId) {
return eventId && firedEvents[eventId];
}
/**
* Mark event as fired for deduplication
*
* @param {string|null} eventId Event ID
*/
function markEventFired(eventId) {
if (eventId) {
firedEvents[eventId] = true;
}
}
/**
* Log warning to console (with safety check)
*
* @param {string} message Warning message
* @param {*} data Additional data to log
*/
function logWarning(message, data) {
if (typeof console !== 'undefined' && console.warn) {
console.warn('[FB Pixel]', message, data);
}
}
/**
* Fire a single event using fbq()
*
* @param {Object} event Event object with name, params, method, eventId
*/
function fireEvent(event) {
var eventData = buildEventData(event);
// Skip if already fired (deduplication)
if (shouldSkipEvent(eventData.eventId)) {
return;
}
// Skip if fbq not available
if (typeof fbq !== 'function') {
logWarning('fbq not available, skipping event:', eventData.name);
return;
}
try {
var params = eventData.params;
// Fire the event with eventID as 4th argument for deduplication
if (eventData.eventId) {
fbq(eventData.method, eventData.name, params, {eventID: eventData.eventId});
} else {
fbq(eventData.method, eventData.name, params);
}
markEventFired(eventData.eventId);
} catch (e) {
logWarning('Event error: ' + eventData.name, e);
}
}
/**
* Fire all queued events from PHP
*/
function fireQueuedEvents() {
var events = data.eventQueue;
if (!events || !Array.isArray(events)) {
return;
}
for (var i = 0; i < events.length; i++) {
try {
fireEvent(events[i]);
} catch (e) {
logWarning('fireQueuedEvents loop error:', e);
}
}
// Clear events after firing to prevent duplicate firing
data.eventQueue = [];
}
// =========================================================================
// WooCommerce Blocks: Store API approach
// =========================================================================
/**
* Process pixel event data from Store API response.
*
* @param {Object} eventData Event data from Store API extensions
*/
function processStoreApiEvent(eventData) {
if (!eventData || !eventData.event) {
return;
}
var params = eventData.params || {};
var event = {
method: 'track',
name: eventData.event,
params: params,
eventId: params.event_id || null
};
fireEvent(event);
}
/**
* Set up fetch interceptor to capture Store API responses.
* Only intercepts cart/add-item requests to fire AddToCart pixel events.
*/
function setupFetchInterceptor() {
var originalFetch = window.fetch;
if (!originalFetch) {
return;
}
window.fetch = function() {
var args = arguments;
var url = args[0];
// Only intercept add-item requests (not general cart requests)
var isAddToCartRequest = typeof url === 'string' &&
(url.indexOf('/wc/store/v1/cart/add-item') !== -1 ||
url.indexOf('/wc/store/cart/add-item') !== -1);
return originalFetch.apply(this, args).then(function(response) {
if (isAddToCartRequest && response.ok) {
// Clone response so we can read it without consuming
response.clone().json().then(function(responseData) {
if (responseData && responseData.extensions && responseData.extensions['facebook-for-woocommerce']) {
processStoreApiEvent(responseData.extensions['facebook-for-woocommerce']);
}
}).catch(function(e) {
logWarning('Store API JSON parse error:', e);
});
}
return response;
});
};
}
/**
* Initialize pixel event handling.
*
* If fbq() is already available, fires queued events immediately.
* If not (e.g. consent manager blocking the SDK), uses Object.defineProperty
* to set a trap on window.fbq — our handler fires automatically the moment
* fbq is assigned, with zero overhead in between. No polling, no timers.
*
* Also sets up Store API interceptor for WooCommerce Blocks AJAX AddToCart.
*/
function init() {
// Set up fetch interceptor for WooCommerce Blocks Store API
setupFetchInterceptor();
if (typeof fbq === 'function') {
fireQueuedEvents();
return;
}
// fbq doesn't exist yet — watch for it (zero overhead, no timers).
// Consent managers block fbq until the
// user accepts. This fires the moment they assign window.fbq.
var _fbq = window.fbq;
Object.defineProperty(window, 'fbq', {
configurable: true,
enumerable: true,
get: function() { return _fbq; },
set: function(value) {
_fbq = value;
if (typeof value === 'function') {
// Restore normal property so FB SDK works normally
Object.defineProperty(window, 'fbq', {
configurable: true,
enumerable: true,
writable: true,
value: value
});
setTimeout(fireQueuedEvents, 0);
}
}
});
}
// Start
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();