array(
array(
'handle' => 'aiforya-sm-tab-types',
'src' => AIFORYA_SM_URL . 'assets/css/tab-types.css',
'ver' => AIFORYA_SM_VERSION,
),
),
);
}
public function render( array $args = [] ): void {
/*
* V786-B — le comportement part par wp_add_inline_script() (grief n°2 de wordpress.org,
* compté ERREUR par le Plugin Check). NOWDOC obligatoire : le corps contient $form,
* $button, $spinner, $result, $row, $op, $btn, $card, $r, $ — un heredoc les viderait
* tous, sans qu aucun contrôle ne crie.
*
* ⭐ GAIN COLLATÉRAL : trois de ces textes passaient par esc_html_e() ÉCHO DIRECT dans
* une chaîne JS — le mauvais échappement pour ce contexte. wp_localize_script les
* encode en JSON, ce qui est le bon.
*/
$handle = 'aiforya-sm-tab-types-js';
wp_register_script( $handle, false, array( 'jquery' ), AIFORYA_SM_VERSION, true );
wp_enqueue_script( $handle );
wp_localize_script(
$handle,
'aiforyaSmTypesL10n',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'rulesNonce' => wp_create_nonce( 'aiforya_sm_admin_nonce' ),
'mappingsSaved' => __( 'Mappings saved successfully!', 'aiforya-schema-markup' ),
'saveError' => __( 'Error while saving.', 'aiforya-schema-markup' ),
'commError' => __( 'Communication error with the server.', 'aiforya-schema-markup' ),
'condPostType' => __( 'Post type', 'aiforya-schema-markup' ),
'condCategory' => __( 'Category', 'aiforya-schema-markup' ),
'condTag' => __( 'Tag', 'aiforya-schema-markup' ),
'condAuthor' => __( 'Author (ID)', 'aiforya-schema-markup' ),
'condUrlRegex' => __( 'URL regex', 'aiforya-schema-markup' ),
'condTitleContains' => __( 'Title contains', 'aiforya-schema-markup' ),
'condMetaField' => __( 'ACF/Meta field', 'aiforya-schema-markup' ),
'valuePlaceholder' => __( 'value', 'aiforya-schema-markup' ),
'editRule' => __( 'Edit rule', 'aiforya-schema-markup' ),
'newRule' => __( 'New Rule', 'aiforya-schema-markup' ),
'unnamedRule' => __( 'Unnamed rule', 'aiforya-schema-markup' ),
'confirmDelete' => __( 'Delete this rule?', 'aiforya-schema-markup' ),
'testing' => __( 'Running test on 30 recent posts…', 'aiforya-schema-markup' ),
'failed' => __( 'Failed.', 'aiforya-schema-markup' ),
)
);
$js = <<<'JS'
jQuery(document).ready(function($) {
var L = window.aiforyaSmTypesL10n || {};
var ajaxurl = window.ajaxurl || L.ajaxUrl;
$('#aiforya-sm-types-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $button = $('#aiforya-sm-save-types');
var $spinner = $form.find('.spinner');
var $result = $form.find('.aiforya-sm-save-result');
$button.prop('disabled', true);
$spinner.addClass('is-active');
$result.removeClass('success error').text('');
// Serialize form natively (PHP $_POST receives schema_type[post]=Article + auto_generate[post]=1).
var formData = $form.serialize() + '&action=aiforya_sm_save_type_mappings'
+ '&nonce=' + encodeURIComponent($form.find('#aiforya_sm_types_nonce').val());
$.ajax({
url: ajaxurl,
type: 'POST',
data: formData,
success: function(response) {
$spinner.removeClass('is-active');
$button.prop('disabled', false);
if (response.success) {
$result.addClass('success').text(response.data.message || L.mappingsSaved);
} else {
$result.addClass('error').text(response.data.message || L.saveError);
}
setTimeout(function() {
$result.fadeOut(function() {
$(this).text('').show();
});
}, 3000);
},
error: function() {
$spinner.removeClass('is-active');
$button.prop('disabled', false);
$result.addClass('error').text(L.commError);
}
});
});
// ============================================================
// F06 Règles avancées — JS handlers
// ============================================================
var rulesNonce = L.rulesNonce;
$('#aiforya-sm-rules-toast-host').appendTo('body');
$('#aiforya-sm-rule-modal').appendTo('body');
function smToast(message, type) {
type = type || 'info';
var $host = $('.aiforya-sm-toast-host').last();
if (!$host.length) $host = $('
').appendTo('body');
var $t = $('', { 'class': 'aiforya-sm-toast aiforya-sm-toast--' + type, role: 'status' }).text(message);
$host.append($t);
setTimeout(function(){ $t.addClass('is-visible'); }, 10);
setTimeout(function(){ $t.removeClass('is-visible'); setTimeout(function(){ $t.remove(); }, 300); }, 4500);
}
var conditionTypes = [
{ value: 'post_type', label: L.condPostType, ops: ['is', 'is_not', 'in'] },
{ value: 'category', label: L.condCategory, ops: ['is', 'is_not'] },
{ value: 'tag', label: L.condTag, ops: ['has', 'has_not'] },
{ value: 'author', label: L.condAuthor, ops: ['is'] },
{ value: 'url_regex', label: L.condUrlRegex, ops: ['matches'] },
{ value: 'title_contains', label: L.condTitleContains, ops: ['contains', 'starts_with', 'ends_with'] },
{ value: 'meta_field', label: L.condMetaField, ops: ['exists', 'not_empty', 'equals', 'contains'] }
];
function escHtml(s){ return String(s == null ? '' : s).replace(/[&<>"']/g, function(c){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
function buildConditionRow(c) {
c = c || { type: 'post_type', op: 'is', value: '', field: '' };
var typeOptions = conditionTypes.map(function(t){
return '';
}).join('');
var typeDef = conditionTypes.find(function(t){ return t.value === c.type; }) || conditionTypes[0];
var opOptions = typeDef.ops.map(function(op){
return '';
}).join('');
var fieldHtml = (c.type === 'meta_field')
? ''
: '';
return '' +
'' +
'' +
fieldHtml +
'' +
'' +
'
';
}
function refreshConditionRow($row) {
var type = $row.find('.aiforya-sm-cond-type').val();
var typeDef = conditionTypes.find(function(t){ return t.value === type; }) || conditionTypes[0];
var $op = $row.find('.aiforya-sm-cond-op').empty();
typeDef.ops.forEach(function(op){
$op.append('');
});
// Field input only for meta_field
$row.find('.aiforya-sm-cond-field').remove();
if (type === 'meta_field') {
$row.find('.aiforya-sm-cond-op').after('');
}
}
$(document).on('change', '.aiforya-sm-cond-type', function(){
refreshConditionRow($(this).closest('.aiforya-sm-rule-cond-row'));
});
function openRuleModal(rule) {
rule = rule || { id: 0, name: '', enabled: 1, priority: 10, schema_type: 'Article', operator: 'AND', conditions: [{ type: 'post_type', op: 'is', value: 'post' }] };
$('.aiforya-sm-rule-modal-title').text(rule.id ? L.editRule : L.newRule);
$('#aiforya-sm-rule-name').val(rule.name);
$('#aiforya-sm-rule-schema-type').val(rule.schema_type);
$('#aiforya-sm-rule-priority').val(rule.priority);
$('#aiforya-sm-rule-operator').val(rule.operator);
$('#aiforya-sm-rule-enabled').prop('checked', !!parseInt(rule.enabled, 10));
var html = '';
(rule.conditions || []).forEach(function(c){ html += buildConditionRow(c); });
$('#aiforya-sm-rule-conditions').html(html);
$('#aiforya-sm-rule-modal').data('editing-id', rule.id || 0).addClass('is-open').fadeIn(120);
}
function closeRuleModal() {
$('#aiforya-sm-rule-modal').removeClass('is-open').fadeOut(120);
}
$(document).on('click', '.aiforya-sm-rule-add', function(){ openRuleModal(); });
$(document).on('click', '.aiforya-sm-rule-edit', function(){
var data = $(this).closest('.aiforya-sm-rule-card').data('rule');
openRuleModal(data);
});
$(document).on('click', '.aiforya-sm-rule-modal-close, .aiforya-sm-rule-modal-backdrop', closeRuleModal);
$(document).on('click', '.aiforya-sm-rule-add-cond', function(){
$('#aiforya-sm-rule-conditions').append(buildConditionRow());
});
$(document).on('click', '.aiforya-sm-cond-remove', function(){
$(this).closest('.aiforya-sm-rule-cond-row').remove();
});
$(document).on('click', '.aiforya-sm-rule-save', function(){
var $btn = $(this);
var conditions = [];
$('#aiforya-sm-rule-conditions .aiforya-sm-rule-cond-row').each(function(){
var $r = $(this);
conditions.push({
type: $r.find('.aiforya-sm-cond-type').val(),
op: $r.find('.aiforya-sm-cond-op').val(),
field: String($r.find('.aiforya-sm-cond-field').val() || ''),
value: String($r.find('.aiforya-sm-cond-value').val() || '')
});
});
var rule = {
id: parseInt($('#aiforya-sm-rule-modal').data('editing-id'), 10) || 0,
name: String($('#aiforya-sm-rule-name').val() || '').trim() || L.unnamedRule,
schema_type: $('#aiforya-sm-rule-schema-type').val(),
priority: parseInt($('#aiforya-sm-rule-priority').val(), 10) || 10,
operator: $('#aiforya-sm-rule-operator').val(),
enabled: $('#aiforya-sm-rule-enabled').prop('checked') ? 1 : 0,
conditions: conditions
};
$btn.prop('disabled', true);
$.post(ajaxurl, { action: 'aiforya_sm_rules_save', nonce: rulesNonce, rule: JSON.stringify(rule) }).done(function(r){
if (r && r.success) {
smToast((r.data && r.data.message) || 'OK', 'success');
closeRuleModal();
setTimeout(function(){ location.reload(); }, 600);
} else {
smToast((r && r.data && r.data.message) || L.failed, 'error');
$btn.prop('disabled', false);
}
});
});
$(document).on('click', '.aiforya-sm-rule-delete', function(){
var $card = $(this).closest('.aiforya-sm-rule-card');
var id = $card.data('rule-id');
window.AiforyaConfirm.ask(L.confirmDelete, function(){
$.post(ajaxurl, { action: 'aiforya_sm_rules_delete', nonce: rulesNonce, rule_id: id }).done(function(r){
if (r && r.success) {
smToast((r.data && r.data.message) || 'OK', 'success');
$card.fadeOut(220, function(){ $(this).remove(); });
} else {
smToast((r && r.data && r.data.message) || L.failed, 'error');
}
});
});
});
$(document).on('click', '.aiforya-sm-rules-test', function(){
var $btn = $(this);
var $result = $('.aiforya-sm-rules-test-result');
$btn.prop('disabled', true);
$result.text(L.testing).css('color', '#94a3b8');
$.post(ajaxurl, { action: 'aiforya_sm_rules_test', nonce: rulesNonce }).done(function(r){
if (r && r.success) {
var d = r.data || {};
$result.css('color', '#a5b4fc').text(d.matched + ' / ' + d.tested_posts + ' posts match ' + d.rules_count + ' rule(s).');
} else {
$result.css('color', '#f87171').text(L.failed);
}
}).always(function(){ $btn.prop('disabled', false); });
});
});
JS;
wp_add_inline_script( $handle, $js );
// Récupération des mappings actuels
$mappings = get_option( 'aiforya_sm_type_mappings', array() );
// Liste des types Schema.org supportés
$schema_types = array(
'Article' => __( 'Article', 'aiforya-schema-markup' ),
'BlogPosting' => __( 'Blog Posting', 'aiforya-schema-markup' ),
'NewsArticle' => __( 'News Article', 'aiforya-schema-markup' ),
'Product' => __( 'Product', 'aiforya-schema-markup' ),
'Recipe' => __( 'Recipe', 'aiforya-schema-markup' ),
'Event' => __( 'Event', 'aiforya-schema-markup' ),
'FAQ' => __( 'FAQ', 'aiforya-schema-markup' ),
'HowTo' => __( 'How-To', 'aiforya-schema-markup' ),
'Review' => __( 'Review', 'aiforya-schema-markup' ),
'VideoObject' => __( 'Video Object', 'aiforya-schema-markup' ),
'LocalBusiness' => __( 'Local Business', 'aiforya-schema-markup' ),
'Organization' => __( 'Organization', 'aiforya-schema-markup' ),
'Person' => __( 'Person', 'aiforya-schema-markup' ),
'WebPage' => __( 'Web Page', 'aiforya-schema-markup' ),
);
// Récupération de tous les post types publics
$post_types = get_post_types( array( 'public' => true ), 'objects' );
// Filtrer les types WordPress système (attachments, nav_menu_item, etc.)
$excluded_types = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block' );
// Helper D2 : pastille d'aide « ? » charte (AiforyaTooltip). $text déjà traduit.
$tip = static function ( $text ) {
printf( '', esc_attr( $text ) );
};
?>
get_rules() : array();
$meta_keys = $rules_service ? $rules_service->discover_meta_keys( 80 ) : array();
$valid_types = class_exists( 'Aiforya_Schema_Markup_Detector' ) ? Aiforya_Schema_Markup_Detector::supported_schema_types() : array();
// $rules_nonce retiré V786-B : le nonce part par wp_localize_script().
?>
- Article :
- Product :
- Recipe :
- Event :
- FAQ :
- HowTo :