' . esc_html__( 'Settings', 'acf-flexible-layout-preview' ) . ''
);
}
return $links;
}
/* =========================================================
1. ACTIVATION / DEACTIVATION
========================================================= */
register_activation_hook( ACFLP_FILE, 'acflp_activate' );
function acflp_activate() {
if ( ! acflp_is_acf_pro_active() ) {
deactivate_plugins( plugin_basename( ACFLP_FILE ) );
wp_die(
esc_html__( 'ACF Layout Preview Manager requires Advanced Custom Fields Pro to be installed and active before this plugin can be activated.', 'acf-flexible-layout-preview' ),
esc_html__( 'ACF Pro Required', 'acf-flexible-layout-preview' ),
[ 'back_link' => true ]
);
}
acflp_init_filesystem();
global $wp_filesystem;
if ( ! $wp_filesystem->exists( acflp_upload_dir() ) ) {
$wp_filesystem->mkdir( acflp_upload_dir() );
}
// Protect directory listing
$htaccess = acflp_upload_dir() . '.htaccess';
if ( ! $wp_filesystem->exists( $htaccess ) ) {
$wp_filesystem->put_contents( $htaccess, "Options -Indexes\n", FS_CHMOD_FILE );
}
}
register_deactivation_hook( ACFLP_FILE, 'acflp_deactivate' );
function acflp_deactivate() {
// Clear cached layouts
delete_transient( 'acflp_all_layouts' );
}
function acflp_is_acf_pro_active(): bool {
return defined( 'ACF_PRO' )
|| class_exists( 'acf_pro' )
|| function_exists( 'acf_pro_get_license' )
|| ( function_exists( 'acf_get_setting' ) && acf_get_setting( 'pro' ) );
}
add_action( 'admin_init', 'acflp_enforce_acf_pro_dependency' );
function acflp_enforce_acf_pro_dependency() {
if ( acflp_is_acf_pro_active() || ! current_user_can( 'activate_plugins' ) ) {
return;
}
deactivate_plugins( plugin_basename( ACFLP_FILE ) );
add_action( 'admin_notices', 'acflp_acf_pro_missing_notice' );
}
function acflp_acf_pro_missing_notice() {
?>
'',
'grid_columns' => 4,
'theme' => 'light',
];
}
function acflp_get_settings(): array {
$settings = get_option( ACFLP_OPTION, [] );
if ( ! is_array( $settings ) ) {
$settings = [];
}
return array_merge( acflp_default_settings(), $settings );
}
function acflp_sanitize_settings( array $settings ): array {
$defaults = acflp_default_settings();
$grid = isset( $settings['grid_columns'] ) ? absint( $settings['grid_columns'] ) : $defaults['grid_columns'];
$theme = isset( $settings['theme'] ) ? sanitize_key( $settings['theme'] ) : $defaults['theme'];
return [
'default_preview_url' => isset( $settings['default_preview_url'] ) ? esc_url_raw( $settings['default_preview_url'] ) : '',
'grid_columns' => min( 6, max( 2, $grid ) ),
'theme' => in_array( $theme, [ 'light', 'dark' ], true ) ? $theme : 'light',
];
}
// Get custom layouts from database
function acflp_get_custom_layouts(): array {
$custom = get_option( ACFLP_CUSTOM_OPTION, [] );
return is_array( $custom ) ? $custom : [];
}
// Save custom layout to database
function acflp_save_custom_layout( string $name, array $data ): void {
$custom = acflp_get_custom_layouts();
$custom[ $name ] = [
'name' => $name,
'label' => sanitize_text_field( $data['label'] ),
'preview_url' => esc_url_raw( $data['preview_url'] ?? '' ),
'preview_source' => 'custom',
];
update_option( ACFLP_CUSTOM_OPTION, $custom );
delete_transient( 'acflp_all_layouts' );
}
// Remove custom layout
function acflp_remove_custom_layout( string $name ): void {
$custom = acflp_get_custom_layouts();
if ( isset( $custom[ $name ] ) ) {
unset( $custom[ $name ] );
update_option( ACFLP_CUSTOM_OPTION, $custom );
delete_transient( 'acflp_all_layouts' );
}
}
/* =========================================================
2. SETTINGS PAGE
========================================================= */
add_action( 'admin_menu', 'acflp_register_menu', 20 );
function acflp_register_menu() {
if ( ! acflp_is_acf_pro_active() ) {
return;
}
add_submenu_page(
'edit.php?post_type=acf-field-group',
__( 'ACF Layout Previews', 'acf-flexible-layout-preview' ),
__( 'Layout Previews', 'acf-flexible-layout-preview' ),
'manage_options',
'acf-layout-preview',
'acflp_settings_page'
);
}
add_action( 'admin_enqueue_scripts', 'acflp_admin_assets' );
function acflp_admin_assets( $hook ) {
if ( ! acflp_is_acf_pro_active() ) {
return;
}
$settings = acflp_get_settings();
// Plugin settings page only - no nonce needed for asset loading
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
if ( 'acf-layout-preview' === $page ) {
wp_enqueue_media();
wp_enqueue_style(
'acflp-admin',
ACFLP_URL . 'assets/admin.css',
[],
ACFLP_VERSION
);
wp_enqueue_script(
'acflp-admin',
ACFLP_URL . 'assets/admin.js',
[ 'jquery' ],
ACFLP_VERSION,
true
);
wp_localize_script( 'acflp-admin', 'acflpData', [
'uploadUrl' => acflp_upload_url(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'acflp_nonce' ),
'settings' => $settings,
'maxFileSize' => ACFLP_MAX_FILE_SIZE,
] );
}
// Popup + inline button: load on ALL admin pages that may have ACF
wp_enqueue_style(
'acflp-popup',
ACFLP_URL . 'assets/popup.css',
[],
ACFLP_VERSION
);
wp_enqueue_script(
'acflp-popup',
ACFLP_URL . 'assets/popup.js',
[ 'jquery' ],
ACFLP_VERSION,
true
);
wp_localize_script( 'acflp-popup', 'acflpData', [
'uploadUrl' => acflp_upload_url(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'acflp_nonce' ),
'settings' => $settings,
'maxFileSize' => ACFLP_MAX_FILE_SIZE,
] );
}
/**
* Redirect wp_handle_upload() to our custom folder inside wp-uploads.
*/
function acflp_custom_upload_dir( array $dirs ): array {
$dirs['subdir'] = '/acf-layout-previews';
$dirs['path'] = $dirs['basedir'] . '/acf-layout-previews';
$dirs['url'] = $dirs['baseurl'] . '/acf-layout-previews';
return $dirs;
}
/* ---- AJAX: Upload image ---- */
add_action( 'wp_ajax_acflp_upload_image', 'acflp_ajax_upload_image' );
function acflp_ajax_upload_image() {
check_ajax_referer( 'acflp_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Unauthorized' );
}
if ( empty( $_FILES['file'] ) || empty( $_POST['layout_name'] ) ) {
wp_send_json_error( 'Missing data' );
}
$layout = sanitize_key( wp_unslash( $_POST['layout_name'] ) );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$file = $_FILES['file'];
// Check for upload errors
if ( $file['error'] !== UPLOAD_ERR_OK ) {
wp_send_json_error( 'Upload error code: ' . (int) $file['error'] );
}
// Check file size
if ( $file['size'] > ACFLP_MAX_FILE_SIZE ) {
wp_send_json_error( 'File too large. Maximum size is 2MB.' );
}
// Use wp_handle_upload() — the WordPress-approved way to handle uploaded files
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$allowed_mimes = [
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif',
];
add_filter( 'upload_dir', 'acflp_custom_upload_dir' );
$uploaded = wp_handle_upload( $file, [
'test_form' => false,
'mimes' => $allowed_mimes,
'unique_filename_callback' => null,
] );
remove_filter( 'upload_dir', 'acflp_custom_upload_dir' );
if ( isset( $uploaded['error'] ) ) {
wp_send_json_error( $uploaded['error'] );
}
// Initialize WP_Filesystem for directory operations and old-file cleanup
acflp_init_filesystem();
global $wp_filesystem;
// Determine final filename based on layout slug
$extension = strtolower( pathinfo( $uploaded['file'], PATHINFO_EXTENSION ) );
$final_name = ( $layout === '__default_preview' ) ? 'default_preview.' . $extension : $layout . '.' . $extension;
$dest = acflp_upload_dir() . $final_name;
// Remove any old files for this slot (any extension)
$prefix = ( $layout === '__default_preview' ) ? 'default_preview.' : $layout . '.';
$old_files = $wp_filesystem->dirlist( acflp_upload_dir() );
if ( $old_files ) {
foreach ( $old_files as $old_file ) {
if ( strpos( $old_file['name'], $prefix ) === 0 ) {
$wp_filesystem->delete( acflp_upload_dir() . $old_file['name'] );
}
}
}
// Rename/move from WP's default upload location to our custom folder
if ( $uploaded['file'] !== $dest ) {
$wp_filesystem->move( $uploaded['file'], $dest, true );
}
// Resize image to a reasonable max size
$image_editor = wp_get_image_editor( $dest );
if ( ! is_wp_error( $image_editor ) ) {
$image_editor->resize( 800, 450, false );
$image_editor->save( $dest );
}
// Clear layout cache
delete_transient( 'acflp_all_layouts' );
$url = acflp_upload_url() . $final_name;
wp_send_json_success( [ 'url' => $url ] );
}
/* ---- AJAX: Delete image ---- */
add_action( 'wp_ajax_acflp_delete_image', 'acflp_ajax_delete_image' );
function acflp_ajax_delete_image() {
check_ajax_referer( 'acflp_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized' );
}
$layout = isset( $_POST['layout_name'] ) ? sanitize_key( wp_unslash( $_POST['layout_name'] ) ) : '';
if ( ! $layout ) {
wp_send_json_error( 'Missing layout name' );
}
// Don't delete default preview files via this method
if ( $layout === '__default_preview' ) {
wp_send_json_error( 'Cannot delete default preview from here.' );
}
acflp_init_filesystem();
global $wp_filesystem;
// Delete files for this layout
$old_files = $wp_filesystem->dirlist( acflp_upload_dir() );
if ( $old_files ) {
foreach ( $old_files as $old_file ) {
if ( strpos( $old_file['name'], $layout . '.' ) === 0 ) {
$wp_filesystem->delete( acflp_upload_dir() . $old_file['name'] );
}
}
}
// Clear cache
delete_transient( 'acflp_all_layouts' );
wp_send_json_success();
}
/* ---- AJAX: Get all layouts from ACF ---- */
add_action( 'wp_ajax_acflp_get_layouts', 'acflp_ajax_get_layouts' );
function acflp_ajax_get_layouts() {
check_ajax_referer( 'acflp_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized' );
}
$layouts = acflp_get_all_flex_layouts();
wp_send_json_success( $layouts );
}
/* ---- AJAX: Save display settings ---- */
add_action( 'wp_ajax_acflp_save_settings', 'acflp_ajax_save_settings' );
function acflp_ajax_save_settings() {
check_ajax_referer( 'acflp_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized' );
}
// Sanitize POST data
$default_preview_url = isset( $_POST['default_preview_url'] ) ? esc_url_raw( wp_unslash( $_POST['default_preview_url'] ) ) : '';
$grid_columns = isset( $_POST['grid_columns'] ) ? intval( wp_unslash( $_POST['grid_columns'] ) ) : 4;
$theme = isset( $_POST['theme'] ) ? sanitize_text_field( wp_unslash( $_POST['theme'] ) ) : 'light';
$settings = acflp_sanitize_settings( [
'default_preview_url' => $default_preview_url,
'grid_columns' => $grid_columns,
'theme' => $theme,
] );
update_option( ACFLP_OPTION, $settings );
// Clear cache when settings change
delete_transient( 'acflp_all_layouts' );
wp_send_json_success( $settings );
}
/* ---- AJAX: Save custom layout ---- */
add_action( 'wp_ajax_acflp_save_custom_layout', 'acflp_ajax_save_custom_layout' );
function acflp_ajax_save_custom_layout() {
check_ajax_referer( 'acflp_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized' );
}
$name = isset( $_POST['name'] ) ? sanitize_key( wp_unslash( $_POST['name'] ) ) : '';
$label = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';
if ( ! $name || ! $label ) {
wp_send_json_error( 'Missing name or label' );
}
acflp_save_custom_layout( $name, [ 'label' => $label ] );
wp_send_json_success();
}
/* =========================================================
3. HELPER — collect all flex-content layouts from ACF
========================================================= */
function acflp_get_all_flex_layouts(): array {
// Check cache first
$cached = get_transient( 'acflp_all_layouts' );
if ( false !== $cached ) {
return $cached;
}
$result = [];
if ( ! function_exists( 'acf_get_field_groups' ) ) return $result;
$settings = acflp_get_settings();
foreach ( acf_get_field_groups() as $group ) {
$fields = acf_get_fields( $group['key'] );
if ( ! $fields ) continue;
acflp_recurse_fields( $fields, $result, $settings );
}
// Add custom layouts
$custom_layouts = acflp_get_custom_layouts();
foreach ( $custom_layouts as $name => $layout ) {
if ( ! isset( $result[ $name ] ) ) {
$preview_url = '';
$preview_source = 'none';
// Check if preview file exists using WP_Filesystem
acflp_init_filesystem();
global $wp_filesystem;
$old_files = $wp_filesystem->dirlist( acflp_upload_dir() );
if ( $old_files ) {
foreach ( $old_files as $old_file ) {
if ( strpos( $old_file['name'], $name . '.' ) === 0 ) {
$preview_url = acflp_upload_url() . $old_file['name'];
$preview_source = 'custom';
break;
}
}
}
if ( ! $preview_url && ! empty( $settings['default_preview_url'] ) ) {
$preview_url = $settings['default_preview_url'];
$preview_source = 'default';
}
$result[ $name ] = [
'name' => $name,
'label' => $layout['label'],
'preview_url' => $preview_url,
'preview_source' => $preview_source,
];
}
}
// Cache for 5 minutes
set_transient( 'acflp_all_layouts', $result, 5 * MINUTE_IN_SECONDS );
return $result;
}
function acflp_recurse_fields( array $fields, array &$result, array $settings = [] ) {
foreach ( $fields as $field ) {
if ( $field['type'] === 'flexible_content' && ! empty( $field['layouts'] ) ) {
foreach ( $field['layouts'] as $layout ) {
$name = $layout['name'];
$label = $layout['label'];
// Find preview image using WP_Filesystem
$preview_url = '';
$preview_source = 'none';
acflp_init_filesystem();
global $wp_filesystem;
$old_files = $wp_filesystem->dirlist( acflp_upload_dir() );
if ( $old_files ) {
foreach ( $old_files as $old_file ) {
if ( strpos( $old_file['name'], $name . '.' ) === 0 ) {
$preview_url = acflp_upload_url() . $old_file['name'];
$preview_source = 'custom';
break;
}
}
}
if ( ! $preview_url && ! empty( $settings['default_preview_url'] ) ) {
$preview_url = $settings['default_preview_url'];
$preview_source = 'default';
}
$result[ $name ] = [
'name' => $name,
'label' => $label,
'preview_url' => $preview_url,
'preview_source' => $preview_source,
];
}
}
// Recurse into sub-fields
if ( ! empty( $field['sub_fields'] ) ) {
acflp_recurse_fields( $field['sub_fields'], $result, $settings );
}
}
}
/* =========================================================
4. SETTINGS PAGE HTML
========================================================= */
function acflp_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) return;
$acf_active = acflp_is_acf_pro_active() && function_exists( 'acf_get_field_groups' );
$settings = acflp_get_settings();
?>
Advanced Custom Fields Pro is not active. Please install and activate ACF Pro to manage layouts.
Layout Display
Control how the Choose a Layout popup looks when editors add a Flexible Content layout.
How To Use
- Create or edit an ACF Flexible Content field and add your layouts.
- Open ACF > Layout Previews and upload one preview image per layout.
- Use Add Custom Layout for layouts that are not discoverable yet.
- Set a Default Preview Image so empty layouts still look polished.
- When editing content, click Add Row/Add Layout and choose layouts from the visual preview modal.
Benefits
- Editors can recognize layouts visually instead of guessing from slugs.
- Preview images are managed from WordPress admin, with no FTP work.
- The default preview keeps the grid and picker consistent while real screenshots are being prepared.
- Search, drag-and-drop uploads, and custom layout registration speed up setup for large ACF builds.
Supported Image Types
$data ) {
if ( ! empty( $data['preview_url'] ) ) {
$map[ $name ] = esc_url( $data['preview_url'] );
}
}
$settings = acflp_get_settings();
$inline = sprintf(
'window.acflpMap = %s; window.acflpSettings = %s;',
wp_json_encode( $map ),
wp_json_encode( $settings )
);
wp_add_inline_script( 'acflp-popup', $inline, 'before' );
}