Versión inicial del plugin FP Genocide Maps (v1.5.2)

CPT geep_map + shortcode [geep_maps] para la sección Architecture of
Genocide de la home: acordeón exclusivo con imagen por mapa, lightbox
con botón de compartir y enlaces #map-<slug>. Soporte WPML (EN/AR) con
herencia de la imagen del idioma original y CSS RTL.
This commit is contained in:
Ana
2026-07-17 12:19:41 +02:00
commit aed764c130
6 changed files with 1003 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
<?php
/**
* Plugin Name: FP Genocide Maps
* Plugin URI: https://freepress.coop
* Description: Manages the "Architecture of Genocide" maps shown on the home page. Each map is a post (title + description + image); the [geep_maps] shortcode renders the accordion + map image replicating the original Divi design, swapping the image when a toggle is opened.
* Version: 1.5.2
* Author: Freepress Coop
* Author URI: https://www.freepress.coop
* Requires PHP: 7.4
* License: GPL-2.0-or-later
* Text Domain: fp-genocide-maps
*
* @package FP_Genocide_Maps
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const FPGM_CPT = 'geep_map';
const FPGM_VERSION = '1.5.2';
/**
* Register the Map custom post type (admin-only, not publicly queryable).
*/
function fpgm_register_cpt() {
register_post_type(
FPGM_CPT,
array(
'labels' => array(
'name' => __( 'Genocide Maps', 'fp-genocide-maps' ),
'singular_name' => __( 'Map', 'fp-genocide-maps' ),
'add_new' => __( 'Add New Map', 'fp-genocide-maps' ),
'add_new_item' => __( 'Add New Map', 'fp-genocide-maps' ),
'edit_item' => __( 'Edit Map', 'fp-genocide-maps' ),
'new_item' => __( 'New Map', 'fp-genocide-maps' ),
'view_item' => __( 'View Map', 'fp-genocide-maps' ),
'search_items' => __( 'Search Maps', 'fp-genocide-maps' ),
'not_found' => __( 'No maps found', 'fp-genocide-maps' ),
'featured_image' => __( 'Map image', 'fp-genocide-maps' ),
'set_featured_image' => __( 'Select map image', 'fp-genocide-maps' ),
),
'public' => false,
'show_ui' => true,
'show_in_menu' => true,
'show_in_rest' => false,
'exclude_from_search' => true,
'menu_icon' => 'dashicons-location-alt',
'menu_position' => 21,
'supports' => array( 'title', 'editor', 'thumbnail', 'page-attributes' ),
'hierarchical' => false,
)
);
}
add_action( 'init', 'fpgm_register_cpt' );
/**
* Front-end assets. The stylesheet replicates the exact Divi-generated design
* of the original section, so it is tiny and safe to load site-wide.
*/
function fpgm_enqueue_assets() {
wp_enqueue_style( 'fpgm-maps', plugins_url( 'assets/css/geep-maps.css', __FILE__ ), array(), FPGM_VERSION );
wp_enqueue_script( 'fpgm-maps', plugins_url( 'assets/js/geep-maps.js', __FILE__ ), array(), FPGM_VERSION, true );
}
add_action( 'wp_enqueue_scripts', 'fpgm_enqueue_assets' );
/**
* [geep_maps] — image column + accordion column, mirroring the original
* "texto mapa" Divi row (columns 14/24 and 2/5, 2% gap).
*/
function fpgm_shortcode() {
$maps = get_posts(
array(
'post_type' => FPGM_CPT,
'posts_per_page' => -1,
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'ASC',
),
'order' => 'ASC',
'suppress_filters' => false, // Let WPML return the current language.
)
);
if ( empty( $maps ) ) {
return '';
}
// Map images are language-independent: when a translation has no image of
// its own, fall back to the original post's image (e.g. Arabic maps show
// the image assigned to the English one). Setting an image on the
// translation still overrides this.
$get_thumb = function ( $post_id ) {
$thumb = get_post_thumbnail_id( $post_id );
if ( ! $thumb ) {
$original = apply_filters( 'wpml_original_element_id', null, $post_id, 'post_' . FPGM_CPT );
if ( $original && (int) $original !== (int) $post_id ) {
$thumb = get_post_thumbnail_id( (int) $original );
}
}
return $thumb;
};
// The first map starts open, so its image (if it has one) is the one
// visible on load. Maps without an image simply leave the gap empty.
$first_thumb = $get_thumb( $maps[0]->ID );
$active_key = $first_thumb ? (string) $first_thumb : '';
// One image per map that has one. The visible size tops out around 660px
// (58% of the 90%-wide row), so tell the browser it never needs the
// full-size file: with `sizes` it picks the smallest sufficient variant.
$sizes = '(max-width: 767px) 100vw, 50vw';
$images_html = '';
$toggles_html = '';
$first = true;
$rendered_keys = array();
foreach ( $maps as $map ) {
$thumb_id = $get_thumb( $map->ID );
$map_key = '';
if ( $thumb_id ) {
$map_key = (string) $thumb_id;
if ( ! in_array( $map_key, $rendered_keys, true ) ) {
$rendered_keys[] = $map_key;
$images_html .= wp_get_attachment_image(
$thumb_id,
'full',
false,
array(
'class' => 'geep-map-img' . ( $map_key === $active_key ? ' is-active is-shown' : '' ),
'data-map' => $map_key,
'loading' => 'lazy',
'sizes' => $sizes,
)
);
}
}
$toggles_html .= sprintf(
'<div class="geep-toggle %s%s" data-map="%s" data-slug="%s">' .
'<h4 class="geep-toggle-title">%s</h4>' .
'<div class="geep-toggle-content">%s</div>' .
'</div>',
$first ? 'geep-toggle--open' : 'geep-toggle--close',
$first ? ' geep-toggle--first' : '',
esc_attr( $map_key ),
esc_attr( urldecode( $map->post_name ) ), // Readable form (Arabic slugs are stored percent-encoded).
esc_html( get_the_title( $map ) ),
wp_kses_post( wpautop( $map->post_content ) )
);
$first = false;
}
// UI labels for the lightbox, in the language of the current page.
$is_ar = 'ar' === apply_filters( 'wpml_current_language', null );
$labels = array(
'share' => $is_ar ? 'مشاركة الخريطة' : 'Share map',
'copied' => $is_ar ? 'تم نسخ الرابط' : 'Link copied!',
'close' => $is_ar ? 'إغلاق' : 'Close',
'zoom' => $is_ar ? 'عرض الخريطة بحجم أكبر' : 'View map larger',
);
return '<div class="geep-maps" data-labels="' . esc_attr( wp_json_encode( $labels ) ) . '">' .
'<div class="geep-maps-col-map"><div class="geep-maps-image"><span class="geep-maps-image-wrap" role="button" tabindex="0" aria-label="' . esc_attr( $labels['zoom'] ) . '">' . $images_html . '</span></div></div>' .
'<div class="geep-maps-col-toggles">' . $toggles_html . '</div>' .
'</div>';
}
add_shortcode( 'geep_maps', 'fpgm_shortcode' );
/* -------------------------------------------------------------------------
* Admin: list-table columns and default-image setting.
* ---------------------------------------------------------------------- */
/**
* Add image + order columns to the Maps list table.
*
* @param array $columns Existing columns.
* @return array
*/
function fpgm_admin_columns( $columns ) {
$new = array();
foreach ( $columns as $key => $label ) {
if ( 'title' === $key ) {
$new['fpgm_image'] = __( 'Map image', 'fp-genocide-maps' );
}
$new[ $key ] = $label;
}
$new['fpgm_order'] = __( 'Order', 'fp-genocide-maps' );
return $new;
}
add_filter( 'manage_' . FPGM_CPT . '_posts_columns', 'fpgm_admin_columns' );
/**
* Render the custom columns.
*
* @param string $column Column key.
* @param int $post_id Post ID.
*/
function fpgm_admin_columns_content( $column, $post_id ) {
if ( 'fpgm_image' === $column ) {
if ( has_post_thumbnail( $post_id ) ) {
echo get_the_post_thumbnail( $post_id, array( 80, 60 ), array( 'style' => 'border-radius:4px;height:auto;' ) );
} else {
echo '<span style="color:#a00;">' . esc_html__( 'No image', 'fp-genocide-maps' ) . '</span>';
}
} elseif ( 'fpgm_order' === $column ) {
echo (int) get_post_field( 'menu_order', $post_id );
}
}
add_action( 'manage_' . FPGM_CPT . '_posts_custom_column', 'fpgm_admin_columns_content', 10, 2 );
/**
* Order the admin list by menu_order, like the front end.
*
* @param WP_Query $query Current query.
*/
function fpgm_admin_order( $query ) {
if ( is_admin() && $query->is_main_query() && FPGM_CPT === $query->get( 'post_type' ) && ! $query->get( 'orderby' ) ) {
$query->set( 'orderby', 'menu_order' );
$query->set( 'order', 'ASC' );
}
}
add_action( 'pre_get_posts', 'fpgm_admin_order' );
/* -------------------------------------------------------------------------
* Activation: seed the current 8 maps (EN + AR, linked in WPML).
* ---------------------------------------------------------------------- */
/**
* Create the initial maps from the content that used to live in the Divi
* toggles, once, on first activation.
*/
function fpgm_activate() {
fpgm_register_cpt();
// Register the CPT as translatable in WPML *before* seeding, otherwise
// wpml_set_element_language_details silently ignores the posts and the
// AR/EN pairs never get linked. Mode 2 = translatable, display the
// original when no translation exists (same as wpml-config.xml).
global $sitepress;
if ( $sitepress && method_exists( $sitepress, 'get_setting' ) ) {
$sync = (array) $sitepress->get_setting( 'custom_posts_sync_option', array() );
$sync[ FPGM_CPT ] = 2;
$sitepress->set_setting( 'custom_posts_sync_option', $sync, true );
}
$existing = get_posts(
array(
'post_type' => FPGM_CPT,
'posts_per_page' => 1,
'post_status' => 'any',
'suppress_filters' => true,
)
);
if ( ! empty( $existing ) ) {
return; // Already seeded.
}
$file = __DIR__ . '/includes/seed-data.json';
if ( ! file_exists( $file ) ) {
return;
}
$data = json_decode( file_get_contents( $file ), true );
if ( empty( $data['en'] ) ) {
return;
}
$has_wpml = has_filter( 'wpml_element_trid' );
foreach ( $data['en'] as $i => $item ) {
$en_id = wp_insert_post(
array(
'post_type' => FPGM_CPT,
'post_status' => 'publish',
'post_title' => wp_strip_all_tags( $item['title'] ),
'post_content' => $item['content'],
'menu_order' => $i + 1,
)
);
if ( ! $en_id || is_wp_error( $en_id ) ) {
continue;
}
if ( $has_wpml ) {
$trid = apply_filters( 'wpml_element_trid', null, $en_id, 'post_' . FPGM_CPT );
do_action(
'wpml_set_element_language_details',
array(
'element_id' => $en_id,
'element_type' => 'post_' . FPGM_CPT,
'trid' => $trid,
'language_code' => 'en',
'source_language_code' => null,
)
);
// Arabic counterpart (same position in the list).
if ( ! empty( $data['ar'][ $i ]['title'] ) ) {
$ar_id = wp_insert_post(
array(
'post_type' => FPGM_CPT,
'post_status' => 'publish',
'post_title' => wp_strip_all_tags( $data['ar'][ $i ]['title'] ),
'post_content' => $data['ar'][ $i ]['content'],
'menu_order' => $i + 1,
)
);
if ( $ar_id && ! is_wp_error( $ar_id ) ) {
do_action(
'wpml_set_element_language_details',
array(
'element_id' => $ar_id,
'element_type' => 'post_' . FPGM_CPT,
'trid' => $trid,
'language_code' => 'ar',
'source_language_code' => 'en',
)
);
}
}
}
}
}
register_activation_hook( __FILE__, 'fpgm_activate' );