ACF: Créer un Contenu Flexible Avancé & Dynamique - hwk
WordPress hwk Blog ACF: Créer un Contenu Flexible Avancé & Dynamique

ACF: Créer un Contenu Flexible Avancé & Dynamique

24 décembre 2017

Le « Contenu Flexible » est probablement la feature la plus intéressante du plugin Advanced Custom Field. Ce groupe de champs permet de créer un puissant outil de mise en page pour vos pages, taxonomies, articles etc… Son administration peut néanmoins se révéler fastidieuse lorsque l’on utilise des clones, groupes et autres type de champs complexes. Dans ce tutoriel nous allons créer un contenu flexible avec des dispositions/groupes de champs ajoutés dynamiquement basé sur des options.

Le Contenu Flexible Dynamique en action

Création des fonctions d’aide

Afin de nous aider à récupérer les données des différentes options, nous allons définir deux fonctions qui nous seront très utile.

  • hwk_acf_get_flexible() va lister tous les groupes de champs convertis en conteneur de Contenu Flexible Dynamique.
  • hwk_acf_get_layouts() nous renvoi la liste des groupes de champs attachés à des Contenus Flexible Dynamiques. Nous optimiserons cette fonction en utilisant acf_set_cache() afin de cacher les résultats. L’action acf/update_filed_group nous servira à vider ce cache (à chaque mise à jour d’un groupe de champs).
<?php
// ACF: Flexible Get
function hwk_acf_get_flexible(){
$return = array();
acf_enable_filter('local');
foreach(acf_get_field_groups() as $group){
if(isset($group['hwk_flexible_dynamique']) && !empty($group['hwk_flexible_dynamique']))
$return[] = $group;
}
return $return;
}
// ACF: Layouts Get
function hwk_acf_get_layouts($value = false){
$cache_key = 'hwk_acf_get_layouts';
if(acf_isset_cache($cache_key)){
$return = acf_get_cache($cache_key);
if($value){
if(isset($return[$value]))
return $return[$value];
else
return array();
}
return $return;
}
$return = array();
foreach(acf_get_field_groups() as $group){
foreach($group['location'] as $location){
foreach($location as $rule){
if($rule['param'] == 'hwk_flexible_content_rule'){
$return[$rule['value']][] = $group['key'];
break;
}
}
}
}
acf_set_cache($cache_key, $return);
if($value){
if(isset($return[$value]))
return $return[$value];
else
return array();
}
return $return;
}
// ACF: Layouts Cache
add_action('acf/update_field_group', 'hwk_acf_get_layouts_delete_cache');
function hwk_acf_get_layouts_delete_cache($group){
acf_delete_cache('hwk_acf_get_layouts');
}

Création des options

Les options du Contenu Flexible Dynamique

Pour faciliter l’administration, nous allons « convertir » les groupes de champs de notre choix en conteneurs de Contenu Flexible Dynamique.

Ces groupes de champs spéciaux devront être vide car nous ajouterons par la suite un champ Contenu Flexible qui clonera d’autres groupes de champs. Pour créer nos metaboxes, nous allons utiliser la même méthode décrite dans le guide pour ajouter des paramètres personnalisés aux groupes de champs.

<?php
add_action('acf/field_group/admin_head', 'hwk_acf_flexible_metabox');
function hwk_acf_flexible_metabox(){
global $field_group;
if(isset($field_group['hwk_flexible_dynamique']) && !empty($field_group['hwk_flexible_dynamique'])){
add_meta_box('acf-field-group-layouts-flexible', __('Flexible Layouts', 'acf'), function(){
global $field_group;
$group = $field_group;
if(!acf_is_field_group_key( $group['key']))
$group['key'] = uniqid('group_');
$name = str_replace('', '_', sanitize_title($group['title']));
$flexible = 'hwk_contenu_flexible_' . $name;
$sub_groups = hwk_acf_get_layouts($flexible);
ob_start(); ?>
<div class="hwk-acf-field-group-fields" style="margin:-15px -12px;">
<div class="acf-field-list">
<?php if(!empty($sub_groups)){ ?>
<?php
$i=0;
$duplicate = array();
foreach($sub_groups as $sub_group_key){ $i++;
if(in_array($sub_group_key, $duplicate))
continue;
$duplicate[] = $sub_group_key; ?>
<?php $sub_group = _acf_get_field_group_by_key($sub_group_key); ?>
<div class="acf-field-object acf-field-object-text">
<div class="handle">
<ul class="acf-hl acf-tbody">
<li class="li-field-order">
<span class="acf-icon"><?php echo $i; ?></span>
</li>
<li class="li-field-label">
<strong>
<a class="edit-field" title="Modifier ce champ" href="<?php echo admin_url('post.php?post='.$sub_group['ID'].'&action=edit'); ?>"><?php echo $sub_group['title']; ?></a>
</strong>
<div class="row-options">
<a class="edit-field" title="Modifier ce champ" href="<?php echo admin_url('post.php?post='.$sub_group['ID'].'&action=edit'); ?>">Modifier</a>
</div>
</li>
<li class="li-field-name">
<?php echo (isset($sub_group['hwk_flexible_group_title']) && !empty($sub_group['hwk_flexible_group_title'])) ? $sub_group['hwk_flexible_group_title'] : $sub_group['title']; ?>
</li>
</ul>
</div>
</div>
<?php } ?>
<?php }else{ ?>
<div class="acf-field-object acf-field-object-text">
<div class="no-fields-message" style="">
Aucun layout. Assignez un groupe de champs au contenu flexible.
</div>
</div>
<?php } ?>
</div>
</div>
<?php
$html = ob_get_clean();
acf_render_field_wrap(array(
'label' => '',
'instructions' => '',
'type' => 'message',
'name' => 'hwk_flexible_layouts',
'prefix' => 'acf_field_group',
'new_lines' => '',
//'message' => ''
'message' => $html
));
?>
<script type="text/javascript">
if( typeof acf !== 'undefined' ) {
acf.postbox.render({
'id': 'acf-field-group-layouts-flexible',
'label': 'top'
});
}
</script>
<?php
}, 'acf-field-group', 'normal', 'high');
}
$match = false;
foreach($field_group['location'] as $location){
foreach($location as $rule){
if($rule['param'] == 'hwk_flexible_content_rule'){
$match = true;
break;
}
}
}
if(!$match && !acf_get_fields($field_group)){
add_meta_box('acf-field-group-options-flexible', __('Flexible Dynamique', 'acf'), function(){
global $field_group;
$group = $field_group;
if(!acf_is_field_group_key( $group['key']))
$group['key'] = uniqid('group_');
acf_render_field_wrap(array(
'label' => __('Activer','acf'),
'instructions' => __('Convertir ce groupe de champs en contenu flexible dynamique','acf'),
'type' => 'true_false',
'name' => 'hwk_flexible_dynamique',
'prefix' => 'acf_field_group',
'value' => (isset($group['hwk_flexible_dynamique'])) ? $group['hwk_flexible_dynamique'] : '',
'ui' => true,
'ui_on_text' => "Oui",
'ui_off_text' => "Non"
));
?>
<script type="text/javascript">
if( typeof acf !== 'undefined' ) {
acf.postbox.render({
'id': 'acf-field-group-options-flexible',
'label': 'left'
});
}
jQuery(document).ready(function($){
if($('[data-name="hwk_flexible_dynamique"] .acf-switch').hasClass('-on'))
$('#acf-field-group-fields.postbox').addClass('acf-hidden');
$('[data-name="hwk_flexible_dynamique"] .acf-switch').click(function(){
if($(this).hasClass('-on')){
$('#acf-field-group-fields.postbox').removeClass('acf-hidden');
$('#acf-field-group-layouts-flexible').addClass('acf-hidden');
}else{
$('#acf-field-group-fields.postbox').addClass('acf-hidden');
$('#acf-field-group-layouts-flexible').removeClass('acf-hidden');
}
});
});
</script>
<?php
}, 'acf-field-group', 'normal', 'high');
}
}

Le résultat est le suivant:

  • Une metabox Flexible Dynamique nous permet de définir si ce groupe de champs doit être converti en « conteneur ». Il n’apparaitra que si le groupe de champs est vide et qu’il n’a pas de condition d’affichage sur un Contenu Flexible Dynamique (réservé pour les groupes de champs « normaux »).
  • Une metabox Flexible Layouts qui liste tous les groupes de champs rattachés à ce Contenu Flexible Dynamique.

Les options des groupes de champs classique

Nous allons maintenant créer des options pour les groupes de champs qui sont rattachés à notre Contenu Flexible dynamique. Un champ titre et image nous permettront de personnaliser son affichage dans le contenu flexible.

<?php
add_action('acf/field_group/admin_head', 'hwk_acf_flexible_layout_metabox');
function hwk_acf_flexible_layout_metabox(){
global $field_group;
$match = false;
foreach($field_group['location'] as $location){
foreach($location as $rule){
if($rule['param'] == 'hwk_flexible_content_rule'){
$match = true;
break;
}
}
}
if(!$match)
return;
add_meta_box('acf-field-group-options-flexible-group', __('Options Flexible', 'acf'), function(){
global $field_group;
if(!acf_is_field_group_key( $field_group['key']))
$field_group['key'] = uniqid('group_');
$group = $field_group;
acf_render_field_wrap(array(
'label' => __('Titre','acf'),
'type' => 'text',
'name' => 'hwk_flexible_group_title',
'prefix' => 'acf_field_group',
'value' => (isset($group['hwk_flexible_group_title'])) ? $group['hwk_flexible_group_title'] : $field_group['title'],
));
acf_render_field_wrap(array(
'label' => __('Image','acf'),
'type' => 'image',
'name' => 'hwk_flexible_group_image',
'prefix' => 'acf_field_group',
'value' => (isset($group['hwk_flexible_group_image'])) ? $group['hwk_flexible_group_image'] : '',
'preview_size' => "thumbnail",
));
?>
<script type="text/javascript">
if( typeof acf !== 'undefined' ) {
acf.postbox.render({
'id': 'acf-field-group-options-flexible-group',
'label': 'left'
});
}
</script>
<?php
}, 'acf-field-group', 'normal', 'high');
}

Création des conditions d’affichage

Afin d’attacher des groupes de champs à un Contenu Flexible Dynamique, nous allons définir une nouvelle condition d’affichage Contenu Flexible. L’utilisateur pourra ainsi sélectionner un ou plusieurs Contenu Flexible à rattacher avec tous les avantages des opérateurs AND et OR de Advanced Custom Fields.

<?php
// ACF: Location Rules Types
add_filter('acf/location/rule_types', 'hwk_acf_flexible_location_rules_types');
function hwk_acf_flexible_location_rules_types($choices){
$choices['Basic']['hwk_flexible_content_rule'] = 'Contenu Flexible';
return $choices;
}
// ACF: Location Rules Values
add_filter('acf/location/rule_values/hwk_flexible_content_rule', 'hwk_acf_flexible_location_rules_values', 99);
function hwk_acf_flexible_location_rules_values($choices){
if($groups = hwk_acf_get_flexible()){
foreach($groups as $group){
if($field = acf_get_fields($group))
$choices[$field[0]['name']] = (isset($field[0]['label']) && !empty($field[0]['label'])) ? $field[0]['label'] : $field[0]['name'];
}
}
return $choices;
}
// ACF: Location Rules Screen
add_filter('acf/location/screen', 'hwk_acf_flexible_location_screen', 10, 2);
function hwk_acf_flexible_location_screen($options, $group){
foreach(hwk_acf_get_layouts() as $k => $group_keys){
if(in_array($group['key'], $group_keys)){
$options['hwk_flexible_content_rule'] = true;
break;
}
}
return $options;
}
// ACF: Location Rules Match
add_filter('acf/location/rule_match/hwk_flexible_content_rule', 'hwk_acf_flexible_location_rules_match', 10, 3);
function hwk_acf_flexible_location_rules_match($match, $rule, $options){
if(isset($options['hwk_flexible_content_rule']) && $options['hwk_flexible_content_rule'] === true)
$match = true;
return $match;
}

Création du Contenu Flexible Dynamique

Nous allons désormais générer les Contenus Flexible Dynamique grâce à la fonction acf_add_local_field(). Pour commencer, nous listons tous les Contenus Flexible Dynamique existants, ensuite nous ajoutons un clone pour tous les tous les groupes de champs ayant une condition d’affichage correspondant à ce Contenu Flexible.

C’est une des spécificités de Advanced Custom Fields, nous ne pouvons pas filtrer à l’avance l’affichage des layouts car nous sommes trop haut dans la hiérarchie des hooks. Pour pallier à ce problème nous allons filtrer cette liste un peu plus tard à l’aide du filtre acf/load_field.

<?php
add_action('init', 'hwk_acf_flexible_create');
function hwk_acf_flexible_create(){
foreach(hwk_acf_get_flexible() as $parent){
$parent_title = $parent['title'];
$parent_name = str_replace('', '_', sanitize_title($parent['title']));
$layouts = array();
$duplicate = array();
foreach(hwk_acf_get_layouts() as $k){
foreach($k as $group_key){
if(in_array($group_key, $duplicate))
continue;
$duplicate[] = $group_key;
$group = _acf_get_field_group_by_key($group_key);
$title = $group['title'];
$name = str_replace('', '_', sanitize_title($group['title']));
if(isset($group['hwk_flexible_group_title']) && !empty($group['hwk_flexible_group_title']))
$title = $group['hwk_flexible_group_title'];
if(isset($group['hwk_flexible_group_image']) && !empty($group['hwk_flexible_group_image'])){
$image = wp_get_attachment_image_src($group['hwk_flexible_group_image'], '280×149');
$title = '<div class="layout_image"><img src="'.$image[0].'" /></div>' . $title;
}
$layouts[] = array(
'layout' => array(
'key' => 'field_hwk_layout_' . $name . '_' . $parent_name,
'name' => 'hwk_layout_' . $name . '_' . $parent_name,
'label' => $title,
'display' => 'block', // block | row | table
'sub_fields' => array(),
'min' => '',
'max' => ''
),
'sub_fields' => array(
'parent' => 'field_hwk_contenu_flexible_' . $parent_name,
'parent_layout' => 'field_hwk_layout_' . $name . '_' . $parent_name,
'key' => 'field_hwk_layout_' . $name . '_clone' . '_' . $parent_name,
'label' => '',
'name' => 'hwk_layout_' . $name. '_clone' . '_' . $parent_name,
'type' => 'clone',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => ''
),
'hide_admin' => 0,
'clone' => array(
0 => $group_key,
),
'display' => 'group',
'layout' => 'row',
'prefix_label' => 0,
'prefix_name' => 1
)
);
}
}
$layout = $sub_fields = array();
foreach($layouts as $val){
$layout[] = $val['layout'];
$sub_fields[] = $val['sub_fields'];
}
acf_add_local_field(array(
'parent' => $parent['key'],
'key' => 'field_hwk_contenu_flexible_' . $parent_name,
'label' => $parent_title,
'name' => 'hwk_contenu_flexible_' . $parent_name,
'type' => 'flexible_content',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'hide_admin' => 0,
'layouts' => $layout,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => '',
),
'user_roles' => array(
0 => 'all',
),
'button_label' => 'Ajouter une section',
'min' => '',
'max' => '',
));
foreach($sub_fields as $sub_field){
acf_add_local_field($sub_field);
}
}
}

Filtrer les Contenus Flexibles Dynamiques

Désormais, tous les Contenus Flexible Dynamique listent les layouts des groupes de champs rattachés. Nous souhaitons filtrer cette liste, afin de respecter les conditions additionnelles des groupes de champs.

Par exemple, si un groupe de champs possède les conditions d’affichage suivantes:

  • Contenu Flexible == Flexible (1)
  • AND
  • Post Type == post

Nous souhaitons alors afficher le Layout uniquement dans le cas où Flexible (1) est affiché et que le Post Type actuel est post. Il faut savoir que notre Flexible (1) peut lui même avoir des conditions d’affichage, ce qui permet de créer des conditions complexes sur plusieurs couches. Pour arriver à nos fins, nous allons utiliser le filtre acf/load_field qui est chargé juste avant le <body>. Celui-ci nous permet d’avoir accès aux fonctions acf_get_location_screen() et acf_match_location_rule(), mais surtout nous permet de filtrer la liste finale des layouts à afficher suivant les conditions établies.

<?php
add_filter('acf/load_field/type=flexible_content', 'hwk_acf_flexible_filter');
function hwk_acf_flexible_filter($field){
if(!is_admin() || wp_doing_ajax() || get_post_type() == 'acf-field-group')
return $field;
$parent = _acf_get_field_group_by_key($field['parent']);
if(!isset($parent['hwk_flexible_dynamique']) || empty($parent['hwk_flexible_dynamique']))
return $field;
$parent_name = str_replace('', '_', sanitize_title($parent['title']));
$match = array();
foreach(hwk_acf_get_layouts($field['name']) as $group_key){
$group = _acf_get_field_group_by_key($group_key);
$name = str_replace('', '_', sanitize_title($group['title']));
$match['layouts'][] = 'field_hwk_layout_' . $name . '_' . $parent_name;
$match['groups'][] = $group;
}
if(!empty($match['layouts'])){
foreach($field['layouts'] as $k => $layout){
if(!in_array($layout['key'], $match['layouts']))
unset($field['layouts'][$k]);
}
}
if(empty($match['layouts']))
$field['layouts'] = array();
if(!empty($match['groups'])){
global $post, $pagenow, $typenow, $plugin_page;
$filters = array();
if(($pagenow === 'post.php' || $pagenow === 'post-new.php') && $typenow !== 'acf'){
$filters['post_id'] = $post->ID;
$filters['post_type'] = $typenow;
}elseif($pagenow === 'admin.php' && isset($plugin_page) && acf_get_options_page($plugin_page)){
$filters['post_id'] = acf_get_valid_post_id('options');
}elseif(($pagenow === 'edit-tags.php' || $pagenow === 'term.php') && isset($_GET['taxonomy'])){
$filters['taxonomy'] = filter_var($_GET['taxonomy'], FILTER_SANITIZE_STRING);
}elseif($pagenow === 'profile.php'){
$filters['user_id'] = get_current_user_id();
$filters['user_form'] = 'edit';
}elseif($pagenow === 'user-edit.php' && isset($_GET['user_id'])){
$filters['user_id'] = filter_var($_GET['user_id'], FILTER_SANITIZE_NUMBER_INT);
$filters['user_form'] = 'edit';
}elseif($pagenow === 'user-new.php'){
$filters['user_id'] = 'new';
$filters['user_form'] = 'edit';
}elseif($pagenow === 'media.php' || $pagenow === 'upload.php'){
$filters['attachment'] = 'All';
}elseif(acf_is_screen('widgets') || acf_is_screen('customize')){
$filters['widget'] = 'all';
}
foreach($match['groups'] as $group){
$name = str_replace('', '_', sanitize_title($group['title']));
$screen = acf_get_location_screen($filters, $group);
$match_or = array();
foreach($group['location'] as $location => $rule){
if(empty($rule))
continue;
$match_and = true;
foreach($rule as $rule_id => $val){
if( !acf_match_location_rule($val, $screen) || ($val['param'] == 'hwk_flexible_content_rule' && $val['value'] != $field['name']) ){
$match_and = false;
break;
}
}
$match_or[] = $match_and;
}
$cancel = false;
foreach($match_or as $and){
if($and === true){
$cancel = true;
break;
}
}
if(!$cancel){
foreach($field['layouts'] as $k => $layout){
if($layout['key'] == 'field_hwk_layout_' . $name . '_' . $parent_name)
unset($field['layouts'][$k]);
}
}
}
}
if(empty($field['layouts']))
$field = array(
'key' => '',
'ID' => '',
'type' => '',
'value' => '',
'name' => 'hwk_hidden_parent',
'_name' => '',
'class' => 'acf-hidden',
'wrapper' => array(
'class' => 'acf-hidden'
)
);
return $field;
}

Javascript & CSS

<?php
add_action('acf/input/admin_head', 'hwk_acf_flexible_head');
function hwk_acf_flexible_head(){ ?>
<script type="text/javascript">
jQuery(document).ready(function($){
$('[data-name="hwk_hidden_parent"]').each(function(){
$(this).closest('.acf-postbox').addClass('acf-hidden');
});
});
</script>
<style type="text/css">
.acf-field-clone[data-name^="hwk_layout_"]{
padding:0;
margin:-1px;
}
.acf-fc-layout-handle .layout_image{
display:none;
}
.acf-fc-popup li a{
padding:12px;
border-bottom:1px solid #222;
text-align:center;
}
.acf-fc-popup li:last-of-type a{
border-bottom:0;
}
.acf-fc-popup li .layout_image{
margin-bottom:7px;
}
.acf-fc-popup li .layout_image img{
max-width:200px;
height:auto;
}
.hwk-acf-field-group-fields {
border: 0 none;
box-shadow: none;
}
.hwk-acf-field-group-fields > .handlediv,
.hwk-acf-field-group-fields > .hndle {
display: none;
}
.hwk-acf-field-group-fields a {
text-decoration: none;
}
.hwk-acf-field-group-fields a:active,
.hwk-acf-field-group-fields a:focus {
outline: none;
box-shadow: none;
}
.hwk-acf-field-group-fields .no-fields-message {
padding: 15px 15px;
background: #fff;
}
.hwk-acf-field-group-fields .li-field-order {
width: 20%;
}
.hwk-acf-field-group-fields .li-field-label {
width: 30%;
}
.hwk-acf-field-group-fields .li-field-name {
width: 25%;
}
.hwk-acf-field-group-fields .li-field-type {
width: 25%;
}
.hwk-acf-field-group-fields .li-field-key {
display: none;
}
.hwk-acf-field-group-fields .acf-field-list-wrap {
border: #DFDFDF solid 1px;
}
.hwk-acf-field-group-fields .acf-field-list {
background: #F9F9F9;
margin-top: -1px;
}
</style>
<?php
}


Konrad Chmielewski

Evangeliste WordPress & Full Stack Developer depuis 10 années.