FEATURE Introducing the thumbnail focal point plugin with its features
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Antoine M 2025-10-16 11:47:35 +02:00
parent 9483cd8e79
commit 17811c8091
21 changed files with 22035 additions and 9 deletions

View File

@ -72,7 +72,12 @@ $issue_related_articles = get_field('articles', $last_issue->ID);
<div class="block-dernieres-dynamiques__issue-thumbnail-wrapper">
<div class="block-dernieres-dynamiques__issue-thumbnail">
<?php echo get_the_post_thumbnail($last_issue->ID, 'full'); ?>
<?php
$focal_position = safe_get_thumbnail_focal_point_css($last_issue->ID);
echo get_the_post_thumbnail($last_issue->ID, 'full', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
</div>
<div class="card-background"></div>
</div>

View File

@ -72,7 +72,12 @@ $issue_related_articles = get_field('articles', $last_issue->ID);
<div class="block-dernieres-dynamiques__issue-thumbnail-wrapper">
<div class="block-dernieres-dynamiques__issue-thumbnail">
<?php echo get_the_post_thumbnail($last_issue->ID, 'full'); ?>
<?php
$focal_position = safe_get_thumbnail_focal_point_css($last_issue->ID);
echo get_the_post_thumbnail($last_issue->ID, 'full', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
</div>
<div class="card-background"></div>
</div>

View File

@ -81,10 +81,14 @@ $query = new WP_Query(array(
</div>
<div class="card-revue__issue-thumbnail-wrapper">
<?php $post_thumbnail_id = get_the_post_thumbnail(get_the_ID(), 'full'); ?>
<div class="card-revue__issue-thumbnail">
<?php if ($post_thumbnail_id) : ?>
<?php echo $post_thumbnail_id; ?>
<?php if (has_post_thumbnail()) : ?>
<?php
$focal_position = safe_get_thumbnail_focal_point_css();
echo get_the_post_thumbnail(get_the_ID(), 'full', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
<?php endif; ?>
</div>
<div class="card-background"></div>

View File

@ -81,10 +81,14 @@ $query = new WP_Query(array(
</div>
<div class="card-revue__issue-thumbnail-wrapper">
<?php $post_thumbnail_id = get_the_post_thumbnail(get_the_ID(), 'full'); ?>
<div class="card-revue__issue-thumbnail">
<?php if ($post_thumbnail_id) : ?>
<?php echo $post_thumbnail_id; ?>
<?php if (has_post_thumbnail()) : ?>
<?php
$focal_position = safe_get_thumbnail_focal_point_css();
echo get_the_post_thumbnail(get_the_ID(), 'full', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
<?php endif; ?>
</div>
<div class="card-background"></div>

View File

@ -0,0 +1,143 @@
# 🚀 Guide de démarrage rapide
## Installation rapide
1. Activez le plugin dans WordPress
2. C'est tout ! Le plugin fonctionne automatiquement 🎉
## Utilisation
### Pour l'éditeur classique (avec ACF)
1. **Éditez un article/page** avec une image mise en avant
2. **Trouvez la meta box "Image mise en avant"** dans la sidebar
3. **Le focal point picker apparaît sous l'image** automatiquement
4. **Cliquez sur l'image** pour définir le point focal
5. **Enregistrez** votre article
```
┌─────────────────────────────────┐
│ Image mise en avant │
├─────────────────────────────────┤
│ [Image de la thumbnail] │
│ ┌─┴─┐ │
│ │ + │ ← Point focal │
│ └───┘ │
│ │
│ ───────────────────────── │
│ Point focal de recadrage │
│ [Picker interactif] │
│ Position : 65% / 40% │
└─────────────────────────────────┘
```
### Pour l'éditeur Gutenberg
1. **Définissez une image mise en avant**
2. **Ouvrez le panneau "Point focal de la miniature"** dans la sidebar
3. **Cliquez sur l'image** pour définir le focal point
4. Le point focal est **sauvegardé automatiquement**
## Utilisation dans vos templates
### Méthode simple
```php
<?php
// Récupérer la position CSS directement
$focal_position = get_thumbnail_focal_point_css();
// Utiliser avec object-position
the_post_thumbnail('large', [
'style' => 'object-fit: cover; object-position: ' . $focal_position
]);
?>
```
### Avec background-image
```php
<div style="
background-image: url(<?php echo get_the_post_thumbnail_url(); ?>);
background-size: cover;
background-position: <?php echo get_thumbnail_focal_point_css(); ?>;
height: 400px;
">
<h1><?php the_title(); ?></h1>
</div>
```
## Fonctions disponibles
| Fonction | Retour | Exemple |
| ----------------------------------------- | ---------------------------- | -------------------------- |
| `get_thumbnail_focal_point($post_id)` | `['x' => 0.65, 'y' => 0.40]` | Valeurs brutes (0 à 1) |
| `get_thumbnail_focal_point_css($post_id)` | `"65% 40%"` | Format CSS prêt à l'emploi |
**Note** : `$post_id` est optionnel. Si omis, utilise l'article courant.
## Exemples de cas d'usage
### 1. Cards d'articles
```php
<article class="card">
<?php
the_post_thumbnail('medium', [
'class' => 'card-image',
'style' => 'width: 100%; height: 200px; object-fit: cover; object-position: ' . get_thumbnail_focal_point_css()
]);
?>
<h2><?php the_title(); ?></h2>
</article>
```
### 2. Hero banner
```php
<section class="hero" style="
background-image: url(<?php echo get_the_post_thumbnail_url(null, 'full'); ?>);
background-position: <?php echo get_thumbnail_focal_point_css(); ?>;
background-size: cover;
">
<div class="hero-content">
<h1><?php the_title(); ?></h1>
</div>
</section>
```
### 3. Grille responsive
```php
<div class="image-grid">
<?php while (have_posts()): the_post(); ?>
<div class="grid-item">
<?php
the_post_thumbnail('large', [
'style' => 'object-fit: cover; object-position: ' . get_thumbnail_focal_point_css()
]);
?>
</div>
<?php endwhile; ?>
</div>
```
## Format des données
Les données sont stockées dans la table `wp_postmeta` :
```json
{
"x": 0.65,
"y": 0.4
}
```
Où :
- `x: 0` = gauche, `x: 0.5` = centre, `x: 1` = droite
- `y: 0` = haut, `y: 0.5` = milieu, `y: 1` = bas
## Support
Pour plus de détails, consultez le fichier `README.md` ou `example-usage.php`.

View File

@ -0,0 +1,187 @@
# Plugin Dynamiques Focal Point
Plugin WordPress pour ajouter un point focal personnalisable aux images mises en avant (thumbnails).
## Installation
1. Activer le plugin dans l'administration WordPress
2. Le plugin fonctionne automatiquement avec :
- **L'éditeur Gutenberg** : un panneau React apparaît dans la sidebar
- **L'éditeur classique (avec ACF)** : le focal point picker s'intègre directement dans la meta box "Image mise en avant"
## Caractéristiques techniques
- ✅ Utilise le composant officiel `FocalPointPicker` de WordPress
- ✅ Interface React pour une expérience utilisateur moderne
- ✅ **S'intègre directement dans la meta box native de thumbnail** (pas de meta box séparée !)
- ✅ Compatible avec tous les post types supportant les thumbnails
- ✅ Fonctionne avec ACF et l'éditeur classique
- ✅ Sauvegarde automatique dans les post meta
## Utilisation dans l'éditeur
### Avec l'éditeur Gutenberg
Après avoir défini une image mise en avant pour votre article/page :
1. Ouvrez le panneau "Point focal de la miniature" dans la sidebar
2. Cliquez sur l'image pour définir le point focal
3. Le point focal sera sauvegardé automatiquement
### Avec l'éditeur classique (ou avec ACF)
Après avoir défini une image mise en avant :
1. Allez dans la meta box **"Image mise en avant"** (celle qui est native à WordPress)
2. Le focal point picker apparaît automatiquement **en dessous de l'image**
3. Cliquez sur l'image pour définir le point focal (utilise le composant React `FocalPointPicker`)
4. Enregistrez votre article/page pour sauvegarder le point focal
**Avantages** :
- 🎯 Tout est au même endroit - pas besoin de chercher une autre meta box
- 🚀 Interface native et familière
- ⚡ Rechargement automatique si vous changez l'image mise en avant
## Utilisation dans le thème
### Récupérer les valeurs du focal point
```php
<?php
$focal_point = get_post_meta(get_the_ID(), 'thumbnail_focal_point', true);
if ($focal_point && is_array($focal_point)) {
$x = $focal_point['x'] ?? 0.5; // Valeur entre 0 et 1
$y = $focal_point['y'] ?? 0.5; // Valeur entre 0 et 1
// Convertir en pourcentage
$x_percent = ($x * 100) . '%';
$y_percent = ($y * 100) . '%';
}
?>
```
### Exemple 1 : Appliquer le focal point avec object-position
```php
<?php
$focal_point = get_post_meta(get_the_ID(), 'thumbnail_focal_point', true);
$object_position = '50% 50%'; // Valeur par défaut (centre)
if ($focal_point && is_array($focal_point)) {
$x_percent = ($focal_point['x'] * 100);
$y_percent = ($focal_point['y'] * 100);
$object_position = $x_percent . '% ' . $y_percent . '%';
}
?>
<div class="post-thumbnail">
<?php
the_post_thumbnail('large', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($object_position) . ';'
]);
?>
</div>
```
### Exemple 2 : Utiliser le focal point comme background-position
```php
<?php
$focal_point = get_post_meta(get_the_ID(), 'thumbnail_focal_point', true);
$bg_position = '50% 50%';
if ($focal_point && is_array($focal_point)) {
$x_percent = ($focal_point['x'] * 100);
$y_percent = ($focal_point['y'] * 100);
$bg_position = $x_percent . '% ' . $y_percent . '%';
}
$thumbnail_url = get_the_post_thumbnail_url(get_the_ID(), 'large');
?>
<div class="post-thumbnail-bg"
style="background-image: url(<?php echo esc_url($thumbnail_url); ?>);
background-position: <?php echo esc_attr($bg_position); ?>;
background-size: cover;">
</div>
```
### Exemple 3 : Fonction helper réutilisable
Ajoutez cette fonction dans votre `functions.php` :
```php
<?php
/**
* Obtenir le focal point d'un post
*
* @param int|null $post_id ID du post (null pour le post courant)
* @return array Tableau avec 'x' et 'y' (valeurs entre 0 et 1)
*/
function get_thumbnail_focal_point($post_id = null) {
if (!$post_id) {
$post_id = get_the_ID();
}
$focal_point = get_post_meta($post_id, 'thumbnail_focal_point', true);
// Valeurs par défaut (centre de l'image)
if (!$focal_point || !is_array($focal_point)) {
return ['x' => 0.5, 'y' => 0.5];
}
return [
'x' => $focal_point['x'] ?? 0.5,
'y' => $focal_point['y'] ?? 0.5
];
}
/**
* Obtenir le focal point formaté pour CSS
*
* @param int|null $post_id ID du post (null pour le post courant)
* @return string Format "50% 50%" pour object-position ou background-position
*/
function get_thumbnail_focal_point_css($post_id = null) {
$focal_point = get_thumbnail_focal_point($post_id);
$x_percent = ($focal_point['x'] * 100);
$y_percent = ($focal_point['y'] * 100);
return $x_percent . '% ' . $y_percent . '%';
}
?>
```
Puis dans vos templates :
```php
<div class="post-thumbnail">
<?php
the_post_thumbnail('large', [
'style' => 'object-fit: cover; object-position: ' . get_thumbnail_focal_point_css() . ';'
]);
?>
</div>
```
## Support des post types
Le plugin enregistre automatiquement les métadonnées du focal point pour tous les post types publics qui supportent les images mises en avant (thumbnails).
## Données sauvegardées
Les données sont sauvegardées dans les post meta sous la clé `thumbnail_focal_point` au format :
```json
{
"x": 0.5,
"y": 0.5
}
```
`x` et `y` sont des nombres décimaux entre 0 et 1 :
- `x: 0` = gauche, `x: 0.5` = centre, `x: 1` = droite
- `y: 0` = haut, `y: 0.5` = milieu, `y: 1` = bas

View File

@ -0,0 +1,62 @@
/**
* Styles pour le focal point picker dans Gutenberg
*/
/* Conteneur principal dans le panneau featured image */
#gutenberg-focal-point-inline-root {
padding: 0;
}
#gutenberg-focal-point-inline-root > div {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #ddd;
}
/* Titre */
#gutenberg-focal-point-inline-root strong {
display: block;
margin-bottom: 12px;
font-size: 13px;
font-weight: 500;
color: #1e1e1e;
}
/* Instructions */
#gutenberg-focal-point-inline-root p {
margin: 0 0 12px;
font-size: 13px;
color: #757575;
line-height: 1.4;
}
/* FocalPointPicker */
#gutenberg-focal-point-inline-root .components-focal-point-picker {
margin-bottom: 12px;
}
/* Position actuelle */
#gutenberg-focal-point-inline-root p:last-child {
margin-top: 12px;
margin-bottom: 0;
}
#gutenberg-focal-point-inline-root p:last-child strong {
display: inline;
color: #2271b1;
font-weight: 600;
margin: 0;
}
/* S'assurer que le picker s'affiche correctement */
.editor-post-featured-image {
position: relative;
}
/* Ajustements responsive */
@media (max-width: 781px) {
#gutenberg-focal-point-inline-root > div {
margin-top: 12px;
padding-top: 12px;
}
}

View File

@ -0,0 +1,72 @@
/**
* Styles personnalisés pour le focal point picker intégré
*/
/* Conteneur principal du focal point picker */
#thumbnail-focal-point-inline-root {
padding: 0;
margin-top: 15px;
}
/* Ajuster l'espacement dans la meta box */
#postimagediv .inside #thumbnail-focal-point-inline-root {
padding-top: 15px;
border-top: 1px solid #dcdcde;
}
/* Titre du focal point picker */
#thumbnail-focal-point-inline-root strong {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: #1d2327;
}
/* Texte d'instructions */
#thumbnail-focal-point-inline-root p {
margin: 0 0 12px;
font-size: 12px;
color: #646970;
line-height: 1.5;
}
/* Conteneur du FocalPointPicker */
#thumbnail-focal-point-inline-root .components-focal-point-picker {
margin-bottom: 10px;
}
/* Ajuster la hauteur du picker si nécessaire */
#thumbnail-focal-point-inline-root .components-focal-point-picker__wrapper {
/* max-height: 300px; */
}
/* Position actuelle */
#thumbnail-focal-point-inline-root p:last-child {
margin-bottom: 0;
margin-top: 8px;
}
/* Style pour la valeur de position */
#thumbnail-focal-point-inline-root p:last-child strong {
display: inline;
color: #2271b1;
font-weight: 600;
margin: 0;
}
/* Responsive - ajuster sur petits écrans */
@media screen and (max-width: 782px) {
#thumbnail-focal-point-inline-root {
margin-top: 12px;
}
#postimagediv .inside #thumbnail-focal-point-inline-root {
padding-top: 12px;
}
}
/* Animation de chargement */
#thumbnail-focal-point-inline-root.loading {
opacity: 0.6;
pointer-events: none;
}

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-element'), 'version' => '5e8f7612c40c2f842dc7');

View File

@ -0,0 +1 @@
(()=>{"use strict";const e=window.wp.element,t=window.wp.components,n=(window.wp.apiFetch,window.ReactJSXRuntime),o=({postId:o,thumbnailUrl:i,initialFocalPoint:a})=>{const[l,d]=(0,e.useState)(a);return(0,e.useEffect)(()=>{let e=document.getElementById("thumbnail_focal_point_x"),t=document.getElementById("thumbnail_focal_point_y"),n=document.getElementById("thumbnail_focal_point_nonce");e||(e=document.createElement("input"),e.type="hidden",e.name="thumbnail_focal_point_x",e.id="thumbnail_focal_point_x",document.querySelector("form#post").appendChild(e)),t||(t=document.createElement("input"),t.type="hidden",t.name="thumbnail_focal_point_y",t.id="thumbnail_focal_point_y",document.querySelector("form#post").appendChild(t)),n||(n=document.createElement("input"),n.type="hidden",n.name="thumbnail_focal_point_nonce",n.id="thumbnail_focal_point_nonce",n.value=window.thumbnailFocalPointData?.nonce||"",document.querySelector("form#post").appendChild(n)),e.value=l.x,t.value=l.y},[l]),(0,n.jsxs)("div",{style:{marginTop:"15px",paddingTop:"15px",borderTop:"1px solid #dcdcde"},children:[(0,n.jsx)("div",{style:{marginBottom:"10px"},children:(0,n.jsx)("strong",{style:{fontSize:"13px",color:"#1d2327"},children:"Point focal de recadrage"})}),(0,n.jsx)("p",{style:{marginBottom:"12px",fontSize:"12px",color:"#646970",marginTop:"8px"},children:"Cliquez sur l'image pour définir le point focal utilisé lors du recadrage."}),(0,n.jsx)("div",{style:{marginBottom:"10px"},children:(0,n.jsx)(t.FocalPointPicker,{url:i,value:l,onChange:e=>{d(e)}})}),(0,n.jsxs)("p",{style:{fontSize:"12px",color:"#646970",marginTop:"8px"},children:["Position :"," ",(0,n.jsxs)("strong",{style:{color:"#2271b1"},children:[Math.round(100*l.x),"% / ",Math.round(100*l.y),"%"]})]})]})};if(window.addEventListener("DOMContentLoaded",()=>{!function(){const t=document.getElementById("postimagediv");if(!t)return;const i=t.querySelector("#set-post-thumbnail");if(!i||!window.thumbnailFocalPointData)return;if(!i.querySelector("img"))return;let a=document.getElementById("thumbnail-focal-point-inline-root");if(!a){a=document.createElement("div"),a.id="thumbnail-focal-point-inline-root";const e=t.querySelector(".inside");e?e.appendChild(a):t.appendChild(a)}const{postId:l,thumbnailUrl:d,focalPointX:c,focalPointY:r}=window.thumbnailFocalPointData;(0,e.render)((0,n.jsx)(o,{postId:l,thumbnailUrl:d,initialFocalPoint:{x:c,y:r}}),a)}(),"undefined"!=typeof jQuery&&(jQuery(document).on("ajaxSuccess",function(e,t,n){n.data&&"string"==typeof n.data&&-1!==n.data.indexOf("action=set-post-thumbnail")&&setTimeout(()=>{location.reload()},300)}),jQuery(document).on("click","#remove-post-thumbnail",function(){setTimeout(()=>{location.reload()},500)}))}),"undefined"!=typeof wp&&wp.media){const e=wp.media.featuredImage;if(e&&e.frame){const t=e.frame;wp.media.featuredImage.frame=function(){return this._frame||(this._frame=t.apply(this,arguments),this._frame.on("select",function(){})),this._frame}}}})();

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-components', 'wp-data', 'wp-element'), 'version' => 'ef86dc326873b7e4dd13');

View File

@ -0,0 +1 @@
(()=>{"use strict";const e=window.wp.element,t=window.wp.components,o=window.wp.data,r=window.ReactJSXRuntime,n=()=>{const{featuredImageId:e,featuredImageUrl:n,focalPoint:i}=(0,o.useSelect)(e=>{const t=e("core/editor").getEditedPostAttribute("featured_media"),o=t?e("core").getMedia(t):null,r=e("core/editor").getEditedPostAttribute("meta");return{featuredImageId:t,featuredImageUrl:o?.source_url||null,focalPoint:r?.thumbnail_focal_point||{x:.5,y:.5}}}),{editPost:d}=(0,o.useDispatch)("core/editor");return e&&n?(0,r.jsxs)("div",{style:{marginTop:"16px",paddingTop:"16px",borderTop:"1px solid #ddd"},children:[(0,r.jsx)("div",{style:{marginBottom:"12px"},children:(0,r.jsx)("strong",{style:{fontSize:"13px",color:"#1e1e1e"},children:"Point focal de recadrage"})}),(0,r.jsx)("p",{style:{marginBottom:"12px",fontSize:"13px",color:"#757575"},children:"Cliquez sur l'image pour définir le point focal utilisé lors du recadrage automatique."}),(0,r.jsx)(t.FocalPointPicker,{url:n,value:i,onChange:e=>{d({meta:{thumbnail_focal_point:e}})}}),(0,r.jsxs)("p",{style:{marginTop:"12px",fontSize:"12px",color:"#757575"},children:["Position :"," ",(0,r.jsxs)("strong",{style:{color:"#2271b1"},children:[Math.round(100*i.x),"% / ",Math.round(100*i.y),"%"]})]})]}):null};function i(){const t=document.querySelector('.editor-post-featured-image, [aria-label="Featured image"]');if(!t)return!1;if(!t.querySelector("img"))return!1;let o=document.getElementById("gutenberg-focal-point-inline-root");return o||(o=document.createElement("div"),o.id="gutenberg-focal-point-inline-root",t.appendChild(o)),(0,e.render)((0,r.jsx)(n,{}),o),!0}const d=new MutationObserver(()=>{i()});window.addEventListener("DOMContentLoaded",()=>{setTimeout(()=>{i()&&document.querySelector(".editor-styles-wrapper, .edit-post-visual-editor")&&d.observe(document.body,{childList:!0,subtree:!0})},500)});let l=0;const a=setInterval(()=>{(i()||l>10)&&clearInterval(a),l++},500)})();

View File

@ -0,0 +1,161 @@
<?php
/**
* Exemples d'utilisation du plugin Focal Point
*
* Ces exemples peuvent être utilisés dans vos templates WordPress
*/
// ============================================
// EXEMPLE 1 : Utiliser object-position avec la thumbnail
// ============================================
?>
<!-- Dans votre template (ex: single.php, archive.php, etc.) -->
<article class="post-card">
<?php if (has_post_thumbnail()): ?>
<div class="post-thumbnail">
<?php
$focal_position = get_thumbnail_focal_point_css();
the_post_thumbnail('large', [
'style' => 'width: 100%; height: 300px; object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
</div>
<?php endif; ?>
<h2><?php the_title(); ?></h2>
<div class="excerpt"><?php the_excerpt(); ?></div>
</article>
<?php
// ============================================
// EXEMPLE 2 : Utiliser background-position
// ============================================
?>
<div class="hero-banner" style="
background-image: url(<?php echo esc_url(get_the_post_thumbnail_url(null, 'full')); ?>);
background-size: cover;
background-position: <?php echo esc_attr(get_thumbnail_focal_point_css()); ?>;
min-height: 400px;
">
<h1><?php the_title(); ?></h1>
</div>
<?php
// ============================================
// EXEMPLE 3 : CSS externe avec variable personnalisée
// ============================================
$focal_point = get_thumbnail_focal_point();
?>
<div class="custom-image" style="
--focal-x: <?php echo esc_attr($focal_point['x'] * 100); ?>%;
--focal-y: <?php echo esc_attr($focal_point['y'] * 100); ?>%;
">
<?php the_post_thumbnail('large'); ?>
</div>
<style>
.custom-image img {
width: 100%;
height: 400px;
object-fit: cover;
object-position: var(--focal-x) var(--focal-y);
}
</style>
<?php
// ============================================
// EXEMPLE 4 : Utilisation dans une boucle WP_Query
// ============================================
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 6
]);
if ($query->have_posts()):
while ($query->have_posts()): $query->the_post();
?>
<div class="grid-item">
<?php
$focal_position = get_thumbnail_focal_point_css(get_the_ID());
if (has_post_thumbnail()):
?>
<div class="item-image">
<?php
the_post_thumbnail('medium', [
'style' => 'width: 100%; height: 200px; object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
</div>
<?php endif; ?>
<h3><?php the_title(); ?></h3>
</div>
<?php
endwhile;
wp_reset_postdata();
endif;
// ============================================
// EXEMPLE 5 : Avec ACF Repeater
// ============================================
if (have_rows('articles_slider')):
while (have_rows('articles_slider')): the_row();
$post_object = get_sub_field('article');
if ($post_object):
$post_id = $post_object->ID;
$focal_position = get_thumbnail_focal_point_css($post_id);
?>
<div class="slide">
<?php
echo get_the_post_thumbnail($post_id, 'large', [
'style' => 'object-fit: cover; object-position: ' . esc_attr($focal_position) . ';'
]);
?>
</div>
<?php
endif;
endwhile;
endif;
// ============================================
// EXEMPLE 6 : Classes CSS avec attributs data
// ============================================
$focal_point = get_thumbnail_focal_point();
?>
<div
class="responsive-image"
data-focal-x="<?php echo esc_attr($focal_point['x']); ?>"
data-focal-y="<?php echo esc_attr($focal_point['y']); ?>">
<?php the_post_thumbnail('large'); ?>
</div>
<style>
.responsive-image {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
}
.responsive-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position:
calc(var(--focal-x, 0.5) * 100%) calc(var(--focal-y, 0.5) * 100%);
}
</style>
<script>
// Appliquer les valeurs de focal point aux variables CSS
document.querySelectorAll('.responsive-image').forEach(el => {
const x = el.dataset.focalX || 0.5;
const y = el.dataset.focalY || 0.5;
el.style.setProperty('--focal-x', x);
el.style.setProperty('--focal-y', y);
});
</script>

View File

@ -0,0 +1,208 @@
<?php
/**
* Plugin Name: Dynamiques Focal Point
* Description: Un plugin pour ajouter un point focal à l'image de la thumbnail
* Author: Deligraph
* Text Domain: dynamiques-thumbnail-focal-point
*/
if (! defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
// Enregistrer les métadonnées pour les post types "revues" et "articles"
add_action('init', function () {
$supported_post_types = ['revues', 'articles'];
foreach ($supported_post_types as $post_type) {
register_post_meta($post_type, 'thumbnail_focal_point', [
'show_in_rest' => [
'schema' => [
'type' => 'object',
'properties' => [
'x' => ['type' => 'number'],
'y' => ['type' => 'number'],
],
],
],
'single' => true,
'type' => 'object',
'auth_callback' => function (): bool {
return current_user_can('edit_posts');
}
]);
}
});
/**
* Obtenir le focal point d'un post
*
* @param int|null $post_id ID du post (null pour le post courant)
* @return array Tableau avec 'x' et 'y' (valeurs entre 0 et 1)
*/
function get_thumbnail_focal_point($post_id = null)
{
if (!$post_id) {
$post_id = get_the_ID();
}
$focal_point = get_post_meta($post_id, 'thumbnail_focal_point', true);
// Valeurs par défaut (centre de l'image)
if (!$focal_point || !is_array($focal_point)) {
return ['x' => 0.5, 'y' => 0.5];
}
return [
'x' => isset($focal_point['x']) ? $focal_point['x'] : 0.5,
'y' => isset($focal_point['y']) ? $focal_point['y'] : 0.5
];
}
/**
* Obtenir le focal point formaté pour CSS
*
* @param int|null $post_id ID du post (null pour le post courant)
* @return string Format "50% 50%" pour object-position ou background-position
*/
function get_thumbnail_focal_point_css($post_id = null)
{
$focal_point = get_thumbnail_focal_point($post_id);
$x_percent = ($focal_point['x'] * 100);
$y_percent = ($focal_point['y'] * 100);
return $x_percent . '% ' . $y_percent . '%';
}
// Enqueue pour l'éditeur Gutenberg (pour "revues" et "articles")
add_action('enqueue_block_editor_assets', function () {
$screen = get_current_screen();
$supported_post_types = ['revues', 'articles'];
if (!$screen || !in_array($screen->post_type, $supported_post_types)) {
return;
}
$asset_file = include(plugin_dir_path(__FILE__) . 'build/index.asset.php');
wp_enqueue_script(
'thumbnail-focal-point-panel',
plugin_dir_url(__FILE__) . 'build/index.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
// Charger les styles personnalisés pour Gutenberg
wp_enqueue_style(
'thumbnail-focal-point-gutenberg-css',
plugin_dir_url(__FILE__) . 'assets/focal-point-gutenberg.css',
['wp-components'],
'1.0.0'
);
// Optionnel : Enqueue les styles si nécessaire
if (file_exists(plugin_dir_path(__FILE__) . 'build/index.css')) {
wp_enqueue_style(
'thumbnail-focal-point-panel-style',
plugin_dir_url(__FILE__) . 'build/index.css',
[],
$asset_file['version']
);
}
});
// Enqueue pour l'éditeur classique (inline dans la meta box de thumbnail, pour "revues" et "articles")
add_action('admin_enqueue_scripts', function ($hook) {
// Seulement sur les pages d'édition de post
if (!in_array($hook, ['post.php', 'post-new.php'])) {
return;
}
global $post;
if (!$post) {
return;
}
// Vérifier que c'est un post type supporté
$supported_post_types = ['revues', 'articles'];
$post_type = get_post_type($post);
if (!in_array($post_type, $supported_post_types)) {
return;
}
// Charger le fichier asset pour les dépendances
$asset_file = include(plugin_dir_path(__FILE__) . 'build/focal-point-inline.asset.php');
wp_enqueue_script(
'thumbnail-focal-point-inline',
plugin_dir_url(__FILE__) . 'build/focal-point-inline.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
// Charger les styles des composants WordPress
wp_enqueue_style('wp-components');
// Charger les styles personnalisés
wp_enqueue_style(
'thumbnail-focal-point-inline-css',
plugin_dir_url(__FILE__) . 'assets/focal-point-inline.css',
['wp-components'],
'1.0.0'
);
// Passer les données au JavaScript
$focal_point = get_post_meta($post->ID, 'thumbnail_focal_point', true);
$x = isset($focal_point['x']) ? $focal_point['x'] : 0.5;
$y = isset($focal_point['y']) ? $focal_point['y'] : 0.5;
$thumbnail_id = get_post_thumbnail_id($post->ID);
$thumbnail_url = $thumbnail_id ? wp_get_attachment_image_url($thumbnail_id, 'large') : '';
wp_localize_script('thumbnail-focal-point-inline', 'thumbnailFocalPointData', [
'postId' => $post->ID,
'thumbnailUrl' => $thumbnail_url,
'focalPointX' => $x,
'focalPointY' => $y,
'nonce' => wp_create_nonce('thumbnail_focal_point_save')
]);
});
// Sauvegarder les données de la meta box
add_action('save_post', function ($post_id) {
// Vérifications de sécurité
if (!isset($_POST['thumbnail_focal_point_nonce'])) {
return;
}
if (!wp_verify_nonce($_POST['thumbnail_focal_point_nonce'], 'thumbnail_focal_point_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Sauvegarder les données
if (isset($_POST['thumbnail_focal_point_x']) && isset($_POST['thumbnail_focal_point_y'])) {
$x = floatval($_POST['thumbnail_focal_point_x']);
$y = floatval($_POST['thumbnail_focal_point_y']);
// S'assurer que les valeurs sont entre 0 et 1
$x = max(0, min(1, $x));
$y = max(0, min(1, $y));
update_post_meta($post_id, 'thumbnail_focal_point', [
'x' => $x,
'y' => $y
]);
}
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
{
"name": "dynamiques-thumbnail-focal-point",
"version": "1.0.0",
"description": "Un plugin pour ajouter un point focal à l'image de la thumbnail",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"lint:js": "wp-scripts lint-js",
"format:js": "wp-scripts format-js"
},
"devDependencies": {
"@wordpress/scripts": "^30.24.0"
}
}

View File

@ -0,0 +1,176 @@
import { render, useState, useEffect } from "@wordpress/element";
import { FocalPointPicker } from "@wordpress/components";
import apiFetch from "@wordpress/api-fetch";
const InlineFocalPointPicker = ({ postId, thumbnailUrl, initialFocalPoint }) => {
const [focalPoint, setFocalPoint] = useState(initialFocalPoint);
// Mettre à jour les champs cachés pour la sauvegarde
useEffect(() => {
// Créer ou mettre à jour les champs cachés
let inputX = document.getElementById("thumbnail_focal_point_x");
let inputY = document.getElementById("thumbnail_focal_point_y");
let inputNonce = document.getElementById("thumbnail_focal_point_nonce");
if (!inputX) {
inputX = document.createElement("input");
inputX.type = "hidden";
inputX.name = "thumbnail_focal_point_x";
inputX.id = "thumbnail_focal_point_x";
document.querySelector("form#post").appendChild(inputX);
}
if (!inputY) {
inputY = document.createElement("input");
inputY.type = "hidden";
inputY.name = "thumbnail_focal_point_y";
inputY.id = "thumbnail_focal_point_y";
document.querySelector("form#post").appendChild(inputY);
}
if (!inputNonce) {
inputNonce = document.createElement("input");
inputNonce.type = "hidden";
inputNonce.name = "thumbnail_focal_point_nonce";
inputNonce.id = "thumbnail_focal_point_nonce";
inputNonce.value = window.thumbnailFocalPointData?.nonce || "";
document.querySelector("form#post").appendChild(inputNonce);
}
inputX.value = focalPoint.x;
inputY.value = focalPoint.y;
}, [focalPoint]);
const handleChange = (newFocalPoint) => {
setFocalPoint(newFocalPoint);
};
return (
<div style={{ marginTop: "15px", paddingTop: "15px", borderTop: "1px solid #dcdcde" }}>
<div style={{ marginBottom: "10px" }}>
<strong style={{ fontSize: "13px", color: "#1d2327" }}>Point focal de recadrage</strong>
</div>
<p style={{ marginBottom: "12px", fontSize: "12px", color: "#646970", marginTop: "8px" }}>
Cliquez sur l'image pour définir le point focal utilisé lors du recadrage.
</p>
<div style={{ marginBottom: "10px" }}>
<FocalPointPicker url={thumbnailUrl} value={focalPoint} onChange={handleChange} />
</div>
<p style={{ fontSize: "12px", color: "#646970", marginTop: "8px" }}>
Position :{" "}
<strong style={{ color: "#2271b1" }}>
{Math.round(focalPoint.x * 100)}% / {Math.round(focalPoint.y * 100)}%
</strong>
</p>
</div>
);
};
// Fonction pour injecter le focal point picker dans la meta box de thumbnail
function injectFocalPointPicker() {
// Attendre que la meta box de thumbnail soit chargée
const thumbnailDiv = document.getElementById("postimagediv");
if (!thumbnailDiv) {
return;
}
// Chercher le lien de la thumbnail pour vérifier qu'il y a une image
const thumbnailLink = thumbnailDiv.querySelector("#set-post-thumbnail");
if (!thumbnailLink || !window.thumbnailFocalPointData) {
return;
}
// Vérifier s'il y a une image
const hasImage = thumbnailLink.querySelector("img");
if (!hasImage) {
return;
}
// Créer le conteneur pour notre composant React
let container = document.getElementById("thumbnail-focal-point-inline-root");
if (!container) {
container = document.createElement("div");
container.id = "thumbnail-focal-point-inline-root";
// Trouver la div inside qui contient le contenu
const insideDiv = thumbnailDiv.querySelector(".inside");
if (insideDiv) {
// Injecter après tous les enfants existants de .inside
insideDiv.appendChild(container);
} else {
// Fallback : injecter directement dans postimagediv
thumbnailDiv.appendChild(container);
}
}
const { postId, thumbnailUrl, focalPointX, focalPointY } = window.thumbnailFocalPointData;
// Render le composant React
render(
<InlineFocalPointPicker
postId={postId}
thumbnailUrl={thumbnailUrl}
initialFocalPoint={{ x: focalPointX, y: focalPointY }}
/>,
container
);
}
// Initialiser quand le DOM est prêt
window.addEventListener("DOMContentLoaded", () => {
injectFocalPointPicker();
// Écouter l'événement AJAX de WordPress pour le changement de featured image
if (typeof jQuery !== "undefined") {
jQuery(document).on("ajaxSuccess", function (event, xhr, settings) {
// Vérifier si c'est l'AJAX de set featured image
if (
settings.data &&
typeof settings.data === "string" &&
settings.data.indexOf("action=set-post-thumbnail") !== -1
) {
// WordPress a sauvegardé la nouvelle thumbnail, on peut recharger
setTimeout(() => {
location.reload();
}, 300);
}
});
// Écouter aussi la suppression de thumbnail
jQuery(document).on("click", "#remove-post-thumbnail", function () {
setTimeout(() => {
location.reload();
}, 500);
});
}
});
// Hook dans la media library pour détecter quand une image est sélectionnée
if (typeof wp !== "undefined" && wp.media) {
// Stocker la référence du frame original
const originalFeaturedImage = wp.media.featuredImage;
if (originalFeaturedImage && originalFeaturedImage.frame) {
const originalFrame = originalFeaturedImage.frame;
// Remplacer la méthode frame
wp.media.featuredImage.frame = function () {
if (this._frame) return this._frame;
this._frame = originalFrame.apply(this, arguments);
// Écouter la sélection dans le frame
this._frame.on("select", function () {
// L'utilisateur a sélectionné une image
// Ne rien faire ici, on attend l'AJAX success
});
return this._frame;
};
}
}

View File

@ -0,0 +1 @@
import "./thumbnail-focal-point";

View File

@ -0,0 +1,113 @@
import { render, useState, useEffect } from "@wordpress/element";
import { FocalPointPicker } from "@wordpress/components";
import { useSelect, useDispatch } from "@wordpress/data";
const InlineGutenbergFocalPointPicker = () => {
// Récupérer l'ID et l'URL de l'image mise en avant
const { featuredImageId, featuredImageUrl, focalPoint } = useSelect((select) => {
const featuredMedia = select("core/editor").getEditedPostAttribute("featured_media");
const media = featuredMedia ? select("core").getMedia(featuredMedia) : null;
const meta = select("core/editor").getEditedPostAttribute("meta");
return {
featuredImageId: featuredMedia,
featuredImageUrl: media?.source_url || null,
focalPoint: meta?.thumbnail_focal_point || { x: 0.5, y: 0.5 },
};
});
const { editPost } = useDispatch("core/editor");
// Ne rien afficher si pas d'image mise en avant
if (!featuredImageId || !featuredImageUrl) {
return null;
}
return (
<div style={{ marginTop: "16px", paddingTop: "16px", borderTop: "1px solid #ddd" }}>
<div style={{ marginBottom: "12px" }}>
<strong style={{ fontSize: "13px", color: "#1e1e1e" }}>Point focal de recadrage</strong>
</div>
<p style={{ marginBottom: "12px", fontSize: "13px", color: "#757575" }}>
Cliquez sur l'image pour définir le point focal utilisé lors du recadrage automatique.
</p>
<FocalPointPicker
url={featuredImageUrl}
value={focalPoint}
onChange={(value) => {
editPost({
meta: {
thumbnail_focal_point: value,
},
});
}}
/>
<p style={{ marginTop: "12px", fontSize: "12px", color: "#757575" }}>
Position :{" "}
<strong style={{ color: "#2271b1" }}>
{Math.round(focalPoint.x * 100)}% / {Math.round(focalPoint.y * 100)}%
</strong>
</p>
</div>
);
};
// Fonction pour injecter le focal point picker dans le panneau de featured image
function injectGutenbergFocalPointPicker() {
// Chercher le panneau de featured image
const featuredImagePanel = document.querySelector('.editor-post-featured-image, [aria-label="Featured image"]');
if (!featuredImagePanel) {
return false;
}
// Vérifier qu'il y a une image
const hasImage = featuredImagePanel.querySelector("img");
if (!hasImage) {
return false;
}
// Créer le conteneur s'il n'existe pas
let container = document.getElementById("gutenberg-focal-point-inline-root");
if (!container) {
container = document.createElement("div");
container.id = "gutenberg-focal-point-inline-root";
featuredImagePanel.appendChild(container);
}
// Render le composant React
render(<InlineGutenbergFocalPointPicker />, container);
return true;
}
// Observer pour détecter quand le panneau de featured image est ajouté/modifié
const observer = new MutationObserver(() => {
injectGutenbergFocalPointPicker();
});
// Démarrer l'observation quand le DOM est prêt
window.addEventListener("DOMContentLoaded", () => {
// Essayer d'injecter immédiatement
setTimeout(() => {
if (injectGutenbergFocalPointPicker()) {
// Si réussi, continuer à observer pour les changements
const editorRoot = document.querySelector(".editor-styles-wrapper, .edit-post-visual-editor");
if (editorRoot) {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
}
}, 500);
});
// Réessayer périodiquement pendant les premières secondes
let retryCount = 0;
const retryInterval = setInterval(() => {
if (injectGutenbergFocalPointPicker() || retryCount > 10) {
clearInterval(retryInterval);
}
retryCount++;
}, 500);

View File

@ -0,0 +1,10 @@
const defaultConfig = require("@wordpress/scripts/config/webpack.config");
const path = require("path");
module.exports = {
...defaultConfig,
entry: {
index: path.resolve(process.cwd(), "src", "index.js"),
"focal-point-inline": path.resolve(process.cwd(), "src", "focal-point-inline.js"),
},
};