Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Last active April 14, 2026 03:43
Show Gist options
  • Select an option

  • Save arenagroove/8219300c211879c1f6eb5896b2e77924 to your computer and use it in GitHub Desktop.

Select an option

Save arenagroove/8219300c211879c1f6eb5896b2e77924 to your computer and use it in GitHub Desktop.
LR ACF Quick Text Editor
<?php
/**
* Plugin Name: LR ACF Quick Text Editor
* Description: Lightweight meta box for editing ACF flexible content text fields directly in the post editor. Also supports root-level ACF fields outside flex content. Configurable via Settings > Quick Text Editor.
* Author: Luis Martinez
* Author URI: https://www.lessrain.com
* Version: 7.3.3
* Requires PHP: 7.4
*
* Changelog 7.3.3 (14/04/2026)
* - Fix: post_types can now be saved as empty (all checkboxes unticked),
* correctly disabling the plugin. Previously empty() fallback in
* lr_acf_quick_settings() treated an explicit [] identically to
* "never saved", restoring the 'page' default on every load.
* - Fix: meta box registration now guarded with get_current_screen() base
* check — prevents registration (and render attempt) on ACF options pages
* which in some ACF versions fire add_meta_boxes on a 'page'-typed screen.
*
* Changelog 7.3.2 (13/04/2026)
* - Fix: wysiwyg/textarea fields reverting when saved via native ACF editor —
* TinyMCE entity encoding (&amp;) and CRLF differences caused false dirty
* detection. $norm() now applies html_entity_decode() + line ending
* normalization before comparing submitted value against page-load original.
*
* Changelog 7.3.1 (12/04/2026)
* - Fix: box visibility now uses field visibility exclusively — previously
* empty filter string caused hidden boxes to reappear incorrectly in
* dirty/empty modes.
* - Fix: form submit calls guarded with null check on #post element.
* - Fix: settings exclusion textareas now render newline-separated for
* cleaner editing of long lists.
* - Fix: filter callback now explicitly hides fields that fail dirty/empty
* mode conditions rather than early-returning with stale display state.
* - Fix: active filter reapplied on textarea input when dirty/empty mode
* is active, preventing stale field visibility after live edits.
* - UI: toolbar counter text truncates with ellipsis on narrow screens.
*
* Changelog 7.3.0 (12/04/2026)
* - New: "Show empty" toolbar button — filters to fields with no content,
* useful for translation and content audit workflows. Mutually exclusive
* with "Show changed" mode.
* - New: Expand all / Collapse all toolbar buttons.
* - New: Toolbar counters — live display of total fields, empty, changed, hidden.
* - New: Multi-line settings parsing — exclusion textareas now accept newlines
* in addition to commas as separators.
* - New: Reset to Defaults button in settings page.
*
* Changelog 7.2.2 (12/04/2026)
* - Fix: sanitize_textarea_field() used for all textarea-sourced settings fields
* (excluded_names, excluded_patterns, excluded_keys and root equivalents) —
* sanitize_text_field() was stripping line breaks in multi-line textarea input.
* - Fix: recursion depth guard added to lr_acf_quick_collect_candidates() —
* prevents infinite recursion on pathological circular clone references.
*
* Changelog 7.2.1 (12/04/2026)
* - UI: exclusion inputs (names, patterns, keys) changed from single-line text
* inputs to resizable textareas for easier editing of long comma-separated lists.
*
* Changelog 7.2.0 (12/04/2026)
* - Fix: save_post now runs at priority 20 with original-value tracking via hidden inputs,
* preventing LR editor from overwriting ACF's own save (priority 10) when fields
* are edited via the native ACF interface.
* - Fix: column-aware meta key matching — clone-prefixed field names (col_1_field_name)
* and regular group fields now resolve correctly without cross-column bleed.
* - Fix: repeater row sort order — items now group by repeater row index first,
* then by candidate order, instead of grouping all instances of each field together.
* - Fix: fields never previously saved now render as empty editable fields via
* synthetic meta key fallback.
* - Fix: excluded_patterns with leading underscore (e.g. _ratio) now correctly
* matches field names without the underscore prefix.
* - Fix: default excluded_patterns/excluded_keys no longer overwritten by saved
* empty arrays — array fields fall back to defaults explicitly.
* - Fix: dedup uses key+parents to avoid dropping legitimate sibling fields
* that share the same field name across different columns.
* - New: excluded_keys — exclude flex fields by ACF field key.
* - New: root_excluded_keys — exclude root fields by ACF field key.
* - New: layouts with zero editable fields are silently skipped (no empty box shown),
* unless debug mode is active.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
/* =============================================================================
DEFAULTS & SETTINGS
============================================================================= */
function lr_acf_quick_defaults(): array {
return [
'flex_field' => 'acf_lr_flexible_content',
'post_types' => [ 'page' ],
'excluded_names' => [ 'notes', 'inline_css_vars', 'inline_css', 'custom_css_class','section_class' ],
'excluded_patterns' => ['_ratio', '_width', '_terms','_intermittent_pattern','_scrub_start','_slides_per_view','_slides_per_view_md_down','_slides_per_view_sm_down'],
'excluded_keys' => ['field_649c39571ee2f_field_644c95ece22df'],
'allowed_types' => [ 'text', 'textarea', 'wysiwyg', 'url', 'email', 'number' ],
'debug_mode' => false,
'collapsed' => false,
'root_fields_enabled' => false,
'root_fields_placement' => 'same',
'root_excluded_names' => [ 'notes', 'inline_css_vars', 'inline_css', 'custom_css_class' ],
'root_excluded_patterns' => ['_ratio'],
'root_excluded_keys' => [],
'root_allowed_types' => [ 'text', 'textarea', 'wysiwyg', 'url', 'email', 'number' ],
];
}
// Merge array fields explicitly.
// Uses array_key_exists against $saved so an explicitly saved empty array
// (e.g. post_types unchecked = plugin disabled) is preserved as-is and
// never silently replaced by the default value.
// Only fields that were NEVER saved (key absent from stored option) fall
// back to defaults — covers fresh installs and newly added settings.
function lr_acf_quick_settings(): array {
$saved = get_option( 'lr_acf_quick_settings', null );
$defaults = lr_acf_quick_defaults();
// First install — nothing saved yet, return defaults immediately.
if ( ! is_array( $saved ) ) {
return $defaults;
}
$merged = wp_parse_args( $saved, $defaults );
$array_fields = [
'excluded_patterns',
'excluded_keys',
'root_excluded_patterns',
'root_excluded_keys',
'excluded_names',
'root_excluded_names',
'allowed_types',
'root_allowed_types',
'post_types',
];
foreach ( $array_fields as $key ) {
if ( ! array_key_exists( $key, $saved ) && ! empty( $defaults[ $key ] ) ) {
$merged[ $key ] = $defaults[ $key ];
}
}
return $merged;
}
function lr_acf_quick_all_types(): array {
return [ 'text', 'textarea', 'wysiwyg', 'url', 'email', 'number' ];
}
/* =============================================================================
SETTINGS PAGE — REGISTRATION
============================================================================= */
add_action( 'admin_menu', function () {
add_options_page(
'LR ACF Quick Text Editor',
'LR ACF Quick Text Editor',
'manage_options',
'lr-acf-quick-text',
'lr_acf_quick_settings_render'
);
} );
/* =============================================================================
SETTINGS PAGE — SAVE
============================================================================= */
add_action( 'admin_init', function () {
if ( ! isset( $_POST['lr_acf_quick_settings_nonce'] ) ) return;
if ( ! wp_verify_nonce( $_POST['lr_acf_quick_settings_nonce'], 'lr_acf_quick_settings_save' ) ) return;
if ( ! current_user_can( 'manage_options' ) ) return;
// Reset defaults
if ( isset( $_POST['lr_acf_reset_defaults'] ) ) {
delete_option( 'lr_acf_quick_settings' );
wp_redirect( add_query_arg( [ 'page' => 'lr-acf-quick-text', 'lr_saved' => '2' ], admin_url( 'options-general.php' ) ) );
exit;
}
$all_types = lr_acf_quick_all_types();
$flex_field = sanitize_text_field( wp_unslash( $_POST['lr_acf_flex_field'] ?? '' ) );
$post_types = isset( $_POST['lr_acf_post_types'] ) && is_array( $_POST['lr_acf_post_types'] )
? array_map( 'sanitize_key', wp_unslash( $_POST['lr_acf_post_types'] ) )
: [];
$allowed_types = isset( $_POST['lr_acf_allowed_types'] ) && is_array( $_POST['lr_acf_allowed_types'] )
? array_values( array_intersect( array_map( 'sanitize_key', wp_unslash( $_POST['lr_acf_allowed_types'] ) ), $all_types ) )
: $all_types;
$split = function( $raw ) {
return array_values( array_filter( array_map( 'trim', preg_split( '/[\n\r,]+/', sanitize_textarea_field( wp_unslash( $raw ) ) ) ) ) );
};
update_option( 'lr_acf_quick_settings', [
'flex_field' => $flex_field,
'post_types' => $post_types,
'excluded_names' => $split( $_POST['lr_acf_excluded_names'] ?? '' ),
'excluded_patterns' => $split( $_POST['lr_acf_excluded_patterns'] ?? '' ),
'excluded_keys' => $split( $_POST['lr_acf_excluded_keys'] ?? '' ),
'allowed_types' => $allowed_types,
'debug_mode' => ! empty( $_POST['lr_acf_debug_mode'] ),
'collapsed' => ! empty( $_POST['lr_acf_collapsed'] ),
'root_fields_enabled' => ! empty( $_POST['lr_acf_root_fields_enabled'] ),
'root_fields_placement' => in_array( $_POST['lr_acf_root_fields_placement'] ?? '', [ 'same', 'separate' ], true )
? $_POST['lr_acf_root_fields_placement']
: 'same',
'root_excluded_names' => $split( $_POST['lr_acf_root_excluded_names'] ?? '' ),
'root_excluded_patterns' => $split( $_POST['lr_acf_root_excluded_patterns'] ?? '' ),
'root_excluded_keys' => $split( $_POST['lr_acf_root_excluded_keys'] ?? '' ),
'root_allowed_types' => isset( $_POST['lr_acf_root_allowed_types'] ) && is_array( $_POST['lr_acf_root_allowed_types'] )
? array_values( array_intersect( array_map( 'sanitize_key', wp_unslash( $_POST['lr_acf_root_allowed_types'] ) ), $all_types ) )
: $all_types,
] );
wp_redirect( add_query_arg( [ 'page' => 'lr-acf-quick-text', 'lr_saved' => '1' ], admin_url( 'options-general.php' ) ) );
exit;
} );
/* =============================================================================
SETTINGS PAGE — RENDER
============================================================================= */
function lr_acf_quick_settings_render(): void {
if ( ! current_user_can( 'manage_options' ) ) return;
$s = lr_acf_quick_settings();
$all_types = lr_acf_quick_all_types();
$post_types = get_post_types( [ 'public' => true ], 'objects' );
?>
<div class="wrap lr-settings-wrap">
<h1>LR ACF Quick Text Editor &mdash; Settings</h1>
<?php if ( ! empty( $_GET['lr_saved'] ) && $_GET['lr_saved'] === '1' ) : ?>
<div class="notice notice-success is-dismissible"><p>Settings saved.</p></div>
<?php elseif ( ! empty( $_GET['lr_saved'] ) && $_GET['lr_saved'] === '2' ) : ?>
<div class="notice notice-success is-dismissible"><p>Settings reset to defaults.</p></div>
<?php endif; ?>
<form method="post" action="">
<?php wp_nonce_field( 'lr_acf_quick_settings_save', 'lr_acf_quick_settings_nonce' ); ?>
<!-- FLEX FIELD -->
<div class="lr-section">
<h2 class="lr-section-title">Flexible Content Field</h2>
<p class="description">The ACF flexible content field name to target. Must match the field name (not label) in ACF.</p>
<input type="text"
name="lr_acf_flex_field"
class="regular-text"
value="<?php echo esc_attr( $s['flex_field'] ); ?>"
placeholder="e.g. acf_lr_flexible_content">
</div>
<!-- POST TYPES -->
<div class="lr-section">
<h2 class="lr-section-title">Post Types</h2>
<p class="description">Show the Quick Text Editor meta box on these post types.</p>
<div class="lr-checks">
<?php foreach ( $post_types as $pt ) : ?>
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_post_types[]"
value="<?php echo esc_attr( $pt->name ); ?>"
<?php checked( in_array( $pt->name, $s['post_types'], true ) ); ?>>
<span><?php echo esc_html( $pt->label ); ?></span>
<code><?php echo esc_html( $pt->name ); ?></code>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- EXCLUDED NAMES -->
<div class="lr-section">
<h2 class="lr-section-title">Excluded Field Names</h2>
<p class="description">Sub-field names to always skip, regardless of type. Comma or newline separated.</p>
<textarea
name="lr_acf_excluded_names"
class="large-text lr-excl-textarea"
placeholder="e.g. notes, inline_css, custom_css_class"
rows="2"><?php echo esc_textarea( implode( "\n", $s['excluded_names'] ) ); ?></textarea>
<p class="description" style="margin-top:10px;">Excluded name patterns &mdash; comma or newline separated substrings or regex. Plain text = contains match. Wrap in <code>/&hellip;/</code> for regex.</p>
<textarea
name="lr_acf_excluded_patterns"
class="large-text lr-excl-textarea"
placeholder="e.g. _ratio, _colors, /^col_\d+/i"
rows="2"><?php echo esc_textarea( implode( "\n", $s['excluded_patterns'] ) ); ?></textarea>
<p class="description" style="margin-top:10px;">Excluded field keys &mdash; comma or newline separated ACF field keys.</p>
<textarea
name="lr_acf_excluded_keys"
class="large-text lr-excl-textarea"
placeholder="e.g. field_653b240a2b453"
rows="2"><?php echo esc_textarea( implode( "\n", $s['excluded_keys'] ) ); ?></textarea>
</div>
<!-- ALLOWED TYPES -->
<div class="lr-section">
<h2 class="lr-section-title">Allowed Field Types</h2>
<p class="description">Only fields of these ACF types will appear in the editor.</p>
<div class="lr-checks">
<?php foreach ( $all_types as $type ) : ?>
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_allowed_types[]"
value="<?php echo esc_attr( $type ); ?>"
<?php checked( in_array( $type, $s['allowed_types'], true ) ); ?>>
<code><?php echo esc_html( $type ); ?></code>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- DISPLAY OPTIONS -->
<div class="lr-section">
<h2 class="lr-section-title">Display Options</h2>
<div class="lr-checks lr-checks--stack">
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_collapsed" value="1"
<?php checked( ! empty( $s['collapsed'] ) ); ?>>
<span>Collapse layout rows by default</span>
</label>
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_debug_mode" value="1"
<?php checked( ! empty( $s['debug_mode'] ) ); ?>>
<span>Enable debug mode</span>
<em class="lr-hint">Shows raw schema info per layout row. Disable when not diagnosing.</em>
</label>
</div>
</div>
<!-- ROOT FIELDS -->
<div class="lr-section">
<h2 class="lr-section-title">Root ACF Fields</h2>
<p class="description">Show non-flexible ACF fields assigned to this post — fields outside the flex content block. Values are resolved via ACF labels where available.</p>
<div class="lr-checks lr-checks--stack">
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_root_fields_enabled" value="1"
id="lr_root_enabled"
<?php checked( ! empty( $s['root_fields_enabled'] ) ); ?>>
<span>Enable root fields editor</span>
</label>
<div class="lr-indent" id="lr_root_placement_wrap" <?php echo empty( $s['root_fields_enabled'] ) ? 'style="display:none"' : ''; ?>>
<p class="description" style="margin-bottom:8px;">Where to show root fields:</p>
<label class="lr-check-label">
<input type="radio" name="lr_acf_root_fields_placement" value="same"
<?php checked( $s['root_fields_placement'], 'same' ); ?>>
<span>Same meta box, below flex content</span>
</label>
<label class="lr-check-label" style="margin-top:6px;">
<input type="radio" name="lr_acf_root_fields_placement" value="separate"
<?php checked( $s['root_fields_placement'], 'separate' ); ?>>
<span>Separate meta box</span>
</label>
<hr style="margin:14px 0;border:none;border-top:1px solid #f0f0f0;">
<p class="description" style="margin-bottom:6px;"><strong>Excluded field names</strong> &mdash; comma or newline separated names to skip.</p>
<textarea
name="lr_acf_root_excluded_names"
class="large-text lr-excl-textarea"
placeholder="e.g. notes, inline_css, custom_css_class"
rows="2"><?php echo esc_textarea( implode( "\n", $s['root_excluded_names'] ) ); ?></textarea>
<p class="description" style="margin-top:10px;margin-bottom:6px;"><strong>Excluded name patterns</strong> &mdash; substrings or regex. Plain text = contains match. Wrap in <code>/&hellip;/</code> for regex.</p>
<textarea
name="lr_acf_root_excluded_patterns"
class="large-text lr-excl-textarea"
placeholder="e.g. _ratio, _colors, /^col_\d+/i"
rows="2"><?php echo esc_textarea( implode( "\n", $s['root_excluded_patterns'] ) ); ?></textarea>
<p class="description" style="margin-top:10px;margin-bottom:6px;"><strong>Excluded field keys</strong> &mdash; comma or newline separated ACF field keys.</p>
<textarea
name="lr_acf_root_excluded_keys"
class="large-text lr-excl-textarea"
placeholder="e.g. field_653b240a2b453"
rows="2"><?php echo esc_textarea( implode( "\n", $s['root_excluded_keys'] ) ); ?></textarea>
<p class="description" style="margin:12px 0 6px;"><strong>Allowed field types</strong></p>
<div class="lr-checks" style="margin-top:0;">
<?php foreach ( $all_types as $type ) : ?>
<label class="lr-check-label">
<input type="checkbox" name="lr_acf_root_allowed_types[]"
value="<?php echo esc_attr( $type ); ?>"
<?php checked( in_array( $type, $s['root_allowed_types'], true ) ); ?>>
<code><?php echo esc_html( $type ); ?></code>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="lr-form-actions">
<?php submit_button( 'Save Settings', 'primary', 'submit', false ); ?>
<button type="submit" name="lr_acf_reset_defaults" value="1"
class="button button-secondary lr-btn-reset"
onclick="return confirm('Reset all settings to defaults? This cannot be undone.');">
Reset to Defaults
</button>
</div>
</form>
</div>
<?php lr_acf_quick_settings_styles(); ?>
<?php
}
/* =============================================================================
META BOX — REGISTRATION
============================================================================= */
add_action( 'add_meta_boxes', function () {
$s = lr_acf_quick_settings();
$post_types = ! empty( $s['post_types'] ) ? $s['post_types'] : [];
if ( empty( $post_types ) ) return;
// Guard: don't register on ACF options pages or any non-post screen.
$screen = get_current_screen();
if ( ! $screen || $screen->base !== 'post' ) return;
add_meta_box(
'lr_acf_quick_text',
'Quick Text Editor',
'lr_acf_quick_render',
$post_types,
'normal',
'low'
);
if ( ! empty( $s['root_fields_enabled'] ) && $s['root_fields_placement'] === 'separate' ) {
add_meta_box(
'lr_acf_quick_root',
'Quick Text Editor &mdash; Root Fields',
'lr_acf_quick_render_root_box',
$post_types,
'normal',
'low'
);
}
} );
/* =============================================================================
PATTERN MATCHER
Tests a field name against a list of substrings or regex patterns.
Plain text = contains match. /pattern/flags = regex match.
============================================================================= */
function lr_acf_quick_matches_pattern( string $name, array $patterns ): bool {
foreach ( $patterns as $pattern ) {
if ( $pattern === '' ) continue;
if ( isset( $pattern[0] ) && $pattern[0] === '/' ) {
if ( @preg_match( $pattern, $name ) ) return true;
} else {
$stripped = ltrim( $pattern, '_' );
if ( strpos( $name, $pattern ) !== false ) return true;
if ( $stripped !== $pattern && strpos( $name, $stripped ) !== false ) return true;
}
}
return false;
}
/* =============================================================================
SCHEMA TRAVERSAL
Walks field definitions recursively to collect all editable leaf fields.
Discovers definitions only — no value reading happens here.
============================================================================= */
function lr_acf_quick_collect_candidates( array $fields, array $s, array $parents = [], int $depth = 0 ): array {
if ( $depth > 15 ) return [];
$out = [];
static $acf_field_cache = [];
foreach ( $fields as $field ) {
$name = $field['name'] ?? '';
$label = $field['label'] ?? $name;
$type = $field['type'] ?? '';
$key = $field['key'] ?? '';
if ( $name !== '' && in_array( $name, $s['excluded_names'], true ) ) continue;
if ( $name !== '' && lr_acf_quick_matches_pattern( $name, $s['excluded_patterns'] ?? [] ) ) continue;
if ( $key !== '' && in_array( $key, $s['excluded_keys'] ?? [], true ) ) continue;
$current_parents = $parents;
if ( $name !== '' ) {
$current_parents[] = $name;
}
if ( in_array( $type, $s['allowed_types'], true ) ) {
$out[] = [
'label' => (string) $label,
'name' => (string) $name,
'key' => (string) $key,
'type' => (string) $type,
'parents' => $parents,
];
}
if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) {
$out = array_merge( $out, lr_acf_quick_collect_candidates( $field['sub_fields'], $s, $current_parents, $depth + 1 ) );
}
if ( ! empty( $field['layouts'] ) && is_array( $field['layouts'] ) ) {
foreach ( $field['layouts'] as $layout ) {
if ( ! empty( $layout['sub_fields'] ) ) {
$out = array_merge( $out, lr_acf_quick_collect_candidates( $layout['sub_fields'], $s, $current_parents, $depth + 1 ) );
}
}
}
if ( ! empty( $field['clone'] ) && is_array( $field['clone'] ) && function_exists( 'acf_get_field' ) ) {
foreach ( $field['clone'] as $cloned_key ) {
if ( ! isset( $acf_field_cache[ $cloned_key ] ) ) {
$acf_field_cache[ $cloned_key ] = acf_get_field( $cloned_key );
}
$cloned = $acf_field_cache[ $cloned_key ];
if ( $cloned ) {
$out = array_merge( $out, lr_acf_quick_collect_candidates( [ $cloned ], $s, $current_parents, $depth + 1 ) );
}
}
}
}
return $out;
}
/* =============================================================================
META KEY MAP
Reads ALL post meta once and builds: field_key => [ meta_key => value ]
ACF stores _meta_key = field_key reference entries — we reverse this.
============================================================================= */
function lr_acf_quick_build_key_map( int $post_id ): array {
$s = lr_acf_quick_settings();
$flex = $s['flex_field'];
$all_meta = get_post_meta( $post_id );
$map = [];
foreach ( $all_meta as $meta_key => $values ) {
if ( empty( $meta_key ) || $meta_key[0] !== '_' ) continue;
$field_key = isset( $values[0] ) ? $values[0] : '';
if ( strpos( $field_key, 'field_' ) !== 0 ) continue;
$base_key = substr( $meta_key, 1 );
if ( ! isset( $all_meta[ $base_key ] ) ) continue;
if ( ! empty( $flex ) && empty( $s['root_fields_enabled'] ) ) {
if ( strpos( $base_key, $flex . '_' ) !== 0 ) continue;
}
$map[ $field_key ][ $base_key ] = isset( $all_meta[ $base_key ][0] ) ? $all_meta[ $base_key ][0] : '';
}
return $map;
}
/* =============================================================================
KEY LOOKUP
Handles composite clone keys by falling back to the final segment.
e.g. field_aaa_field_bbb_field_ccc → field_ccc
============================================================================= */
function lr_acf_quick_lookup( array $key_map, string $field_key ): array {
if ( isset( $key_map[ $field_key ] ) ) {
return $key_map[ $field_key ];
}
if ( substr_count( $field_key, 'field_' ) > 1 ) {
$segments = explode( '_field_', $field_key );
$final_key = 'field_' . end( $segments );
if ( isset( $key_map[ $final_key ] ) ) {
return $key_map[ $final_key ];
}
}
return [];
}
/* =============================================================================
META BOX — RENDER
============================================================================= */
function lr_acf_quick_render( WP_Post $post ): void {
if ( ! function_exists( 'have_rows' ) || ! function_exists( 'acf_get_field' ) ) {
echo '<p class="lr-notice">ACF is not active.</p>';
return;
}
$s = lr_acf_quick_settings();
$flex = $s['flex_field'];
if ( empty( $flex ) ) {
echo '<p class="lr-notice">No flex field configured. <a href="' . esc_url( admin_url( 'options-general.php?page=lr-acf-quick-text' ) ) . '">Open Settings</a></p>';
return;
}
$field = acf_get_field( $flex );
if ( ! $field || empty( $field['layouts'] ) ) {
echo '<p class="lr-notice">Flexible content field <code>' . esc_html( $flex ) . '</code> not found.</p>';
return;
}
if ( ! have_rows( $flex, $post->ID ) ) {
echo '<p class="lr-notice">No rows in this flexible content field.</p>';
return;
}
$key_map = lr_acf_quick_build_key_map( $post->ID );
wp_nonce_field( 'lr_acf_quick_save', 'lr_acf_quick_nonce' );
echo '<div class="lr-toolbar">';
echo '<button type="button" id="lr_refresh_btn" class="button button-secondary">&#8635; Save &amp; Refresh</button>';
echo '<span id="lr_dirty_count" class="lr-dirty-count" style="display:none"></span>';
echo '<button type="button" id="lr_filter_dirty" class="button lr-btn-dirty" style="display:none">Show changed</button>';
echo '<button type="button" id="lr_filter_empty" class="button lr-btn-dirty">Show empty</button>';
echo '<button type="button" id="lr_expand_all" class="button lr-btn-utility">Expand all</button>';
echo '<button type="button" id="lr_collapse_all" class="button lr-btn-utility">Collapse all</button>';
echo '<input type="text" id="lr_filter" class="lr-filter" placeholder="Filter fields&hellip;">';
echo '<span id="lr_toolbar_counts" class="lr-toolbar-counts"></span>';
$settings_url = admin_url( 'options-general.php?page=lr-acf-quick-text' );
echo '<a href="' . esc_url( $settings_url ) . '" class="lr-settings-link" title="Quick Text Editor Settings">&#9881;</a>';
echo '</div>';
echo '<div class="lr-qte-wrap">';
$row_index = 0;
$layout_index = [];
$open_attr = empty( $s['collapsed'] ) ? ' open' : '';
while ( have_rows( $flex, $post->ID ) ) {
the_row();
$row_index++;
$layout = get_row_layout();
$layout_index[ $layout ] = ( $layout_index[ $layout ] ?? 0 ) + 1;
$layout_def = null;
foreach ( $field['layouts'] as $def ) {
if ( isset( $def['name'] ) && $def['name'] === $layout ) {
$layout_def = $def;
break;
}
}
if ( ! $layout_def ) continue;
$row_prefix = $flex . '_' . ( $row_index - 1 ) . '_';
$candidates = lr_acf_quick_collect_candidates( $layout_def['sub_fields'] ?? [], $s );
$unique_candidates = [];
$seen_keys = [];
foreach ( $candidates as $c ) {
$dedup_key = $c['key'] . '|' . implode( '|', $c['parents'] );
if ( ! isset( $seen_keys[ $dedup_key ] ) ) {
$unique_candidates[] = $c;
$seen_keys[ $dedup_key ] = true;
}
}
$items = [];
$rendered_meta_keys = [];
foreach ( $unique_candidates as $cand_index => $candidate ) {
$meta_entries = lr_acf_quick_lookup( $key_map, $candidate['key'] );
$col = ! empty( $candidate['parents'] ) ? $candidate['parents'][0] : '';
$col_prefix = $row_prefix . ( $col !== '' ? $col . '_' : '' );
$match_name = $candidate['name'];
if ( $col !== '' && strpos( $match_name, $col . '_' ) === 0 ) {
$match_name = substr( $match_name, strlen( $col ) + 1 );
}
$name_rx = '/^(?:\d+_)?' . preg_quote( $match_name, '/' ) . '$/';
$row_instances = [];
foreach ( $meta_entries as $meta_key => $value ) {
if ( strpos( $meta_key, $col_prefix ) !== 0 ) continue;
$suffix = substr( $meta_key, strlen( $col_prefix ) );
if ( ! preg_match( $name_rx, $suffix ) ) continue;
$row_instances[ $meta_key ] = $value;
}
ksort( $row_instances );
if ( empty( $row_instances ) ) {
$synthetic_key = $col_prefix . $match_name;
$row_instances[ $synthetic_key ] = '';
}
$instance_count = count( $row_instances );
$instance_num = 0;
foreach ( $row_instances as $meta_key => $value ) {
if ( isset( $rendered_meta_keys[ $meta_key ] ) ) continue;
if ( ! is_string( $value ) ) continue;
$rendered_meta_keys[ $meta_key ] = true;
$instance_num++;
$rep_index = 0;
if ( $col !== '' ) {
$after_prefix = substr( $meta_key, strlen( $row_prefix ) );
if ( preg_match( '/^' . preg_quote( $col, '/' ) . '_(\d+)_/', $after_prefix, $m ) ) {
$rep_index = (int) $m[1];
}
}
$label = $candidate['label'];
if ( ! empty( $candidate['parents'] ) ) {
$label = implode( ' / ', $candidate['parents'] ) . ' / ' . $label;
}
if ( $instance_count > 1 ) {
$label .= ' #' . $instance_num;
}
$items[] = [
'input_name' => 'lr_acf_quick_meta[' . esc_attr( $meta_key ) . ']',
'label' => $label,
'meta' => esc_html( $candidate['name'] ) . ' &middot; ' . esc_html( $candidate['type'] ),
'meta_key' => $meta_key,
'type' => $candidate['type'],
'value' => $value,
'empty' => trim( (string) $value ) === '',
'rep_index' => $rep_index,
'cand_index' => $cand_index,
];
}
}
usort( $items, function( $a, $b ) {
if ( $a['rep_index'] !== $b['rep_index'] ) {
return $a['rep_index'] <=> $b['rep_index'];
}
return $a['cand_index'] <=> $b['cand_index'];
} );
$heading = esc_html( $layout_def['label'] ?? $layout );
$heading .= ' <span class="lr-row-index">#' . $layout_index[ $layout ] . ' &mdash; row ' . $row_index . '</span>';
if ( ! $items ) continue;
echo '<details class="lr-box"' . $open_attr . '>';
echo '<summary class="lr-box-header">' . $heading . '</summary>';
echo '<div class="lr-box-body">';
foreach ( $items as $item ) {
lr_acf_quick_render_field_item( $item, $s );
}
if ( ! empty( $s['debug_mode'] ) ) {
echo '<details class="lr-debug">';
echo '<summary class="lr-debug-summary">&#128269; Debug: schema candidates for this layout</summary>';
echo '<table class="lr-debug-table">';
echo '<thead><tr><th>Label</th><th>Name</th><th>Type</th><th>Key</th><th>Parents</th></tr></thead><tbody>';
foreach ( $unique_candidates as $c ) {
echo '<tr>';
echo '<td>' . esc_html( $c['label'] ) . '</td>';
echo '<td><code>' . esc_html( $c['name'] ) . '</code></td>';
echo '<td><code>' . esc_html( $c['type'] ) . '</code></td>';
echo '<td><code>' . esc_html( $c['key'] ) . '</code></td>';
echo '<td><code>' . esc_html( implode( ' → ', $c['parents'] ) ?: '—' ) . '</code></td>';
echo '</tr>';
}
echo '</tbody></table></details>';
}
echo '</div></details>';
}
echo '</div>';
if ( ! empty( $s['root_fields_enabled'] ) && $s['root_fields_placement'] === 'same' ) {
lr_acf_quick_render_root_fields( $post->ID, $key_map, $s );
}
lr_acf_quick_meta_styles();
lr_acf_quick_meta_scripts();
}
/* =============================================================================
ROOT FIELDS — COLLECTOR
Iterates the key_map and returns all ACF fields NOT inside the flex field.
Labels are resolved via acf_get_field(); meta key is used as fallback.
Only allowed types are included; excluded names and patterns are respected.
============================================================================= */
function lr_acf_quick_collect_root_fields( array $key_map, array $s ): array {
$items = [];
$flex = $s['flex_field'];
$seen = [];
static $field_cache = [];
foreach ( $key_map as $field_key => $entries ) {
if ( ! array_key_exists( $field_key, $field_cache ) ) {
$resolved = null;
if ( function_exists( 'acf_get_field' ) ) {
$resolved = acf_get_field( $field_key );
if ( ! $resolved && substr_count( $field_key, 'field_' ) > 1 ) {
$segments = explode( '_field_', $field_key );
$resolved = acf_get_field( 'field_' . end( $segments ) );
}
}
$field_cache[ $field_key ] = $resolved;
}
$field_def = $field_cache[ $field_key ];
if ( ! $field_def ) continue;
$label = $field_def['label'] ?? '';
$type = $field_def['type'] ?? '';
$field_name = $field_def['name'] ?? '';
$parent = $field_def['parent'] ?? '';
$group_label = 'Ungrouped';
if ( $parent && function_exists( 'acf_get_field_group' ) ) {
static $group_cache = [];
if ( ! array_key_exists( $parent, $group_cache ) ) {
$group = acf_get_field_group( $parent );
$group_cache[ $parent ] = ( $group && ! empty( $group['title'] ) ) ? $group['title'] : 'Ungrouped';
}
$group_label = $group_cache[ $parent ];
}
if ( ! $type || ! in_array( $type, $s['root_allowed_types'], true ) ) continue;
if ( $field_name && in_array( $field_name, $s['root_excluded_names'], true ) ) continue;
if ( $field_name && lr_acf_quick_matches_pattern( $field_name, $s['root_excluded_patterns'] ?? [] ) ) continue;
if ( $field_key && in_array( $field_key, $s['root_excluded_keys'] ?? [], true ) ) continue;
$valid_entries = [];
foreach ( $entries as $meta_key => $value ) {
if ( ! empty( $flex ) && strpos( $meta_key, $flex . '_' ) === 0 ) continue;
if ( ! is_string( $value ) ) continue;
if ( isset( $seen[ $meta_key ] ) ) continue;
$valid_entries[ $meta_key ] = $value;
}
if ( empty( $valid_entries ) ) continue;
$entry_count = count( $valid_entries );
$instance_num = 0;
foreach ( $valid_entries as $meta_key => $value ) {
$seen[ $meta_key ] = true;
$instance_num++;
$display_label = $label ?: ( $field_name ?: $meta_key );
if ( $entry_count > 1 ) {
$display_label .= ' #' . $instance_num;
}
$items[] = [
'input_name' => 'lr_acf_quick_meta[' . esc_attr( $meta_key ) . ']',
'label' => $display_label,
'meta' => esc_html( $field_name ?: $meta_key ) . ' &middot; ' . esc_html( $type ),
'meta_key' => $meta_key,
'field_key' => $field_key,
'field_name' => $field_name,
'type' => $type,
'group_label' => $group_label,
'value' => $value,
'empty' => trim( $value ) === '',
];
}
}
usort( $items, function ( $a, $b ) {
$g = strcmp( strtolower( $a['group_label'] ), strtolower( $b['group_label'] ) );
return $g !== 0 ? $g : strcmp( strtolower( $a['label'] ), strtolower( $b['label'] ) );
} );
return $items;
}
/* =============================================================================
ROOT FIELDS — RENDER HELPER
Shared by both inline (same box) and separate meta box rendering.
============================================================================= */
function lr_acf_quick_render_root_fields( int $post_id, array $key_map, array $s ): void {
$items = lr_acf_quick_collect_root_fields( $key_map, $s );
$open_attr = empty( $s['collapsed'] ) ? ' open' : '';
echo '<details class="lr-box lr-box--root"' . $open_attr . '>';
echo '<summary class="lr-box-header">Root Fields <span class="lr-row-index">ACF fields outside flexible content</span></summary>';
echo '<div class="lr-box-body">';
if ( $items ) {
$groups = [];
foreach ( $items as $item ) {
$groups[ $item['group_label'] ][] = $item;
}
$multiple_groups = count( $groups ) > 1;
foreach ( $groups as $group_title => $group_items ) {
if ( $multiple_groups ) {
echo '<details class="lr-root-group" open>';
echo '<summary class="lr-root-group-title">' . esc_html( $group_title ) . '</summary>';
echo '<div class="lr-root-group-body">';
}
foreach ( $group_items as $item ) {
lr_acf_quick_render_field_item( $item, $s );
}
if ( $multiple_groups ) {
echo '</div></details>';
}
}
} else {
echo '<p class="lr-empty">No editable root ACF fields found on this post.</p>';
}
if ( ! empty( $s['debug_mode'] ) ) {
echo '<details class="lr-debug">';
echo '<summary class="lr-debug-summary">&#128269; Debug: root field map</summary>';
echo '<table class="lr-debug-table">';
echo '<thead><tr><th>Group</th><th>Label</th><th>Field Name</th><th>Type</th><th>Field Key</th><th>Meta Key</th></tr></thead><tbody>';
foreach ( $items as $item ) {
echo '<tr>';
echo '<td>' . esc_html( $item['group_label'] ) . '</td>';
echo '<td>' . esc_html( $item['label'] ) . '</td>';
echo '<td><code>' . esc_html( $item['field_name'] ) . '</code></td>';
echo '<td><code>' . esc_html( $item['type'] ) . '</code></td>';
echo '<td><code>' . esc_html( $item['field_key'] ) . '</code></td>';
echo '<td><code>' . esc_html( $item['meta_key'] ) . '</code></td>';
echo '</tr>';
}
echo '</tbody></table></details>';
}
echo '</div></details>';
}
/* =============================================================================
ROOT FIELDS — SEPARATE META BOX CALLBACK
============================================================================= */
function lr_acf_quick_render_root_box( WP_Post $post ): void {
if ( ! function_exists( 'acf_get_field' ) ) {
echo '<p class="lr-notice">ACF is not active.</p>';
return;
}
$s = lr_acf_quick_settings();
$key_map = lr_acf_quick_build_key_map( $post->ID );
wp_nonce_field( 'lr_acf_quick_save', 'lr_acf_quick_nonce' );
echo '<div class="lr-toolbar">';
echo '<button type="button" id="lr_refresh_btn_root" class="button button-secondary">&#8635; Save &amp; Refresh</button>';
echo '<input type="text" id="lr_filter_root" class="lr-filter" placeholder="Filter fields&hellip;">';
echo '</div>';
echo '<div class="lr-qte-wrap">';
lr_acf_quick_render_root_fields( $post->ID, $key_map, $s );
echo '</div>';
lr_acf_quick_meta_styles();
echo '<script>
document.addEventListener("DOMContentLoaded", function() {
var btn = document.getElementById("lr_refresh_btn_root");
if (btn) btn.addEventListener("click", function(e) {
e.preventDefault();
document.getElementById("post").submit();
});
var filter = document.getElementById("lr_filter_root");
if (filter) {
document.querySelectorAll("#lr_acf_quick_root .lr-field").forEach(function(el) {
el.dataset.search = el.textContent.toLowerCase();
});
filter.addEventListener("input", function() {
var v = this.value.toLowerCase();
document.querySelectorAll("#lr_acf_quick_root .lr-field").forEach(function(el) {
el.style.display = el.dataset.search.includes(v) ? "" : "none";
});
});
}
});
</script>';
}
/* =============================================================================
FIELD ITEM RENDERER (update 11/04/2026)
Shared helper — renders a single field textarea with label and meta.
============================================================================= */
function lr_acf_quick_render_field_item( array $item, array $s ): void {
$rows_attr = $item['type'] === 'text' ? 2 : 5;
echo '<div class="lr-field lr-type-' . esc_attr( $item['type'] ) . ( $item['empty'] ? ' lr-field--empty' : '' ) . '">';
echo '<label class="lr-field-label">';
echo '<span class="lr-label-text">' . esc_html( $item['label'] ) . '</span>';
echo '<span class="lr-field-meta">' . $item['meta'] . '</span>';
echo '</label>';
if ( ! empty( $s['debug_mode'] ) ) {
echo '<div class="lr-meta-key">' . esc_html( $item['meta_key'] ) . '</div>';
}
echo '<input type="hidden" name="lr_acf_quick_orig[' . esc_attr( $item['meta_key'] ) . ']" value="' . esc_attr( $item['value'] ) . '">';
echo '<textarea name="' . $item['input_name'] . '" class="lr-textarea" rows="' . $rows_attr . '">'
. esc_textarea( $item['value'] )
. '</textarea>';
echo '</div>';
}
/* =============================================================================
SAVE
============================================================================= */
add_action( 'save_post', function ( $post_id ) {
if ( ! isset( $_POST['lr_acf_quick_nonce'] ) ) return;
if ( ! wp_verify_nonce( $_POST['lr_acf_quick_nonce'], 'lr_acf_quick_save' ) ) return;
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
if ( empty( $_POST['lr_acf_quick_meta'] ) || ! is_array( $_POST['lr_acf_quick_meta'] ) ) return;
$s = lr_acf_quick_settings();
$flex = $s['flex_field'];
$originals = isset( $_POST['lr_acf_quick_orig'] ) && is_array( $_POST['lr_acf_quick_orig'] )
? wp_unslash( $_POST['lr_acf_quick_orig'] )
: [];
$norm = function( $v ) {
$v = str_replace( [ "\r\n", "\r" ], "\n", (string) $v );
return html_entity_decode( $v, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
};
foreach ( $_POST['lr_acf_quick_meta'] as $meta_key => $value ) {
if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $meta_key ) ) continue;
if ( isset( $meta_key[0] ) && $meta_key[0] === '_' ) continue;
$is_flex_key = ! empty( $flex ) && strpos( $meta_key, $flex . '_' ) === 0;
if ( ! $is_flex_key && empty( $s['root_fields_enabled'] ) ) continue;
$value = wp_unslash( $value );
if ( ! array_key_exists( $meta_key, $originals ) ) continue;
if ( $norm( $value ) === $norm( $originals[ $meta_key ] ) ) continue;
update_post_meta( $post_id, $meta_key, $value );
}
}, 20 );
/* =============================================================================
META BOX STYLES
============================================================================= */
function lr_acf_quick_meta_styles(): void {
echo '<style>
.lr-qte-wrap { margin-top: 12px; }
.lr-toolbar {
position: sticky;
top: 32px;
z-index: 10;
background: #fff;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
padding: 8px 0 8px;
border-bottom: 1px solid #f0f0f0;
}
.lr-filter {
flex: 1;
max-width: 220px;
font-size: 12px;
height: 30px;
padding: 0 8px;
border: 1px solid #dcdcde;
border-radius: 3px;
}
.lr-filter:focus { border-color: #9ec2e6; outline: none; box-shadow: 0 0 0 1px #9ec2e6; }
.lr-toolbar-counts {
font-size: 11px;
color: #888;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.lr-settings-link {
font-size: 16px;
color: #999;
text-decoration: none;
line-height: 1;
}
.lr-settings-link:hover { color: #333; }
.lr-box {
margin-bottom: 12px;
background: #fff;
border: 1px solid #dcdcde;
border-left: 4px solid #dcdcde;
border-radius: 3px;
transition: border-color 0.2s ease;
}
.lr-box[open] { border-left-color: #2271b1; }
.lr-box-header {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px 10px 26px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
background: #f6f7f7;
border-bottom: 1px solid #dcdcde;
list-style: none;
user-select: none;
}
.lr-box-header:hover { background: #eef4f9; }
.lr-box-header::before {
content: "▸";
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
color: #888;
transition: transform 0.15s ease;
}
.lr-box[open] > .lr-box-header::before { transform: translateY(-50%) rotate(90deg); }
.lr-row-index { font-weight: 400; color: #888; font-size: 11px; }
.lr-box-body { padding: 14px; background: #fff; }
.lr-root-group {
margin-bottom: 6px;
margin-left: 8px;
border: 1px solid #f0f0f0;
border-left: 3px solid #eee;
border-radius: 3px;
}
.lr-root-group[open] { border-left-color: #999; }
.lr-root-group-title {
position: relative;
display: block;
width: 100%;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #666;
padding: 8px 12px 8px 22px;
background: #fafafa;
cursor: pointer;
user-select: none;
list-style: none;
}
.lr-root-group-title:hover { background: #eef4f9; }
.lr-root-group-title::before {
content: "▸";
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
color: #888;
transition: transform 0.15s ease;
}
.lr-root-group[open] > .lr-root-group-title::before { transform: translateY(-50%) rotate(90deg); }
.lr-root-group-body { padding: 14px; background: #fafafa; }
.lr-field {
margin-bottom: 14px;
margin-left: 14px;
background: #fff;
}
.lr-field:last-child { margin-bottom: 0; }
.lr-field-label {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 5px;
gap: 8px;
flex-wrap: wrap;
}
.lr-label-text { font-weight: 600; font-size: 12px; }
.lr-field-meta { font-size: 10px; color: #aaa; font-family: monospace; }
.lr-textarea {
width: 100%;
font-family: monospace;
font-size: 12px;
resize: vertical;
box-sizing: border-box;
}
.lr-excl-textarea {
font-family: monospace;
font-size: 12px;
min-height: 52px;
resize: vertical;
line-height: 1.5;
padding: 6px 8px;
box-sizing: border-box;
}
.lr-type-text .lr-textarea { min-height: 42px; }
.lr-type-textarea .lr-textarea,
.lr-type-wysiwyg .lr-textarea { min-height: 100px; }
.lr-field--empty .lr-textarea { background: #fff8f8; }
.lr-field--dirty .lr-textarea { background: #fffbe6; }
.lr-dirty-count {
font-size: 11px;
font-weight: 600;
color: #b45309;
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 3px;
padding: 2px 8px;
white-space: nowrap;
}
.lr-dirty-count--saving {
color: #1e6a1e;
background: #f0fdf0;
border-color: #86efac;
}
.lr-btn-dirty { font-size: 11px !important; height: 26px; line-height: 24px; padding: 0 8px !important; }
.lr-btn-dirty--active { background: #fef3c7 !important; border-color: #fcd34d !important; color: #b45309 !important; }
.lr-btn-utility { font-size: 11px !important; height: 26px; line-height: 24px; padding: 0 8px !important; }
.lr-meta-key {
font-size: 10px;
color: #c0c0c0;
font-family: monospace;
margin-bottom: 4px;
word-break: break-all;
}
.lr-empty { color: #999; font-style: italic; margin: 0; font-size: 12px; }
.lr-notice { color: #888; font-style: italic; margin: 8px 0; }
.lr-debug {
margin-top: 16px;
border-top: 1px dashed #e0e0e0;
padding-top: 12px;
}
.lr-debug-summary {
cursor: pointer;
font-size: 11px;
font-weight: 600;
color: #b32d2e;
user-select: none;
}
.lr-debug-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
margin-top: 10px;
}
.lr-debug-table th,
.lr-debug-table td {
border: 1px solid #eee;
padding: 5px 8px;
text-align: left;
vertical-align: top;
}
.lr-debug-table th { background: #f6f7f7; font-weight: 600; }
.lr-debug-table code { font-size: 10px; word-break: break-all; }
</style>';
}
/* =============================================================================
META BOX SCRIPTS
============================================================================= */
function lr_acf_quick_meta_scripts(): void {
echo '<script>
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
var form = document.getElementById("post");
if (form) form.submit();
}
});
var btn = document.getElementById("lr_refresh_btn");
if (btn) {
btn.addEventListener("click", function(e) {
e.preventDefault();
var dc = document.getElementById("lr_dirty_count");
if (dc) {
dc.textContent = "Saving\u2026";
dc.style.display = "";
dc.classList.add("lr-dirty-count--saving");
}
var form = document.getElementById("post");
if (form) form.submit();
});
}
var dirtyCount = document.getElementById("lr_dirty_count");
var dirtyBtn = document.getElementById("lr_filter_dirty");
var emptyBtn = document.getElementById("lr_filter_empty");
var expandBtn = document.getElementById("lr_expand_all");
var collapseBtn = document.getElementById("lr_collapse_all");
var countsEl = document.getElementById("lr_toolbar_counts");
var filterEl = document.getElementById("lr_filter");
var dirtyModeOn = false;
var emptyModeOn = false;
var currentFilter = "";
document.querySelectorAll(".lr-textarea").forEach(function(ta) {
ta.dataset.initialValue = ta.value;
});
var postForm = document.getElementById("post");
if (postForm) {
postForm.addEventListener("submit", function() {
document.querySelectorAll(".lr-textarea").forEach(function(ta) {
ta.dataset.initialValue = ta.value;
});
});
}
function lrUpdateCounts() {
var fields = document.querySelectorAll(".lr-field");
var total = fields.length;
var empty = 0;
var changed = 0;
var hidden = 0;
fields.forEach(function(el) {
var ta = el.querySelector(".lr-textarea");
if (ta && ta.value.trim() === "") empty++;
if (el.classList.contains("lr-field--dirty")) changed++;
if (el.style.display === "none") hidden++;
});
if (countsEl) {
countsEl.textContent = total + " fields | " + empty + " empty | " + changed + " changed" + (hidden > 0 ? " | " + hidden + " hidden" : "");
}
}
function lrUpdateDirty() {
var count = 0;
document.querySelectorAll(".lr-textarea").forEach(function(ta) {
var changed = ta.value !== ta.dataset.initialValue;
ta.closest(".lr-field").classList.toggle("lr-field--dirty", changed);
if (changed) count++;
});
if (dirtyCount) {
dirtyCount.textContent = count > 0 ? count + " unsaved" : "";
dirtyCount.style.display = count > 0 ? "" : "none";
dirtyCount.classList.remove("lr-dirty-count--saving");
}
if (dirtyBtn) {
dirtyBtn.style.display = count > 0 ? "" : "none";
if (count === 0 && dirtyModeOn) lrSetDirtyMode(false);
}
if (dirtyModeOn || emptyModeOn) {
lrApplyFilter(currentFilter);
} else {
lrUpdateCounts();
}
}
function lrSetDirtyMode(on) {
dirtyModeOn = on;
if (on && emptyModeOn) lrSetEmptyMode(false);
if (dirtyBtn) {
dirtyBtn.textContent = on ? "Show all" : "Show changed";
dirtyBtn.classList.toggle("lr-btn-dirty--active", on);
}
lrApplyFilter(currentFilter);
}
function lrSetEmptyMode(on) {
emptyModeOn = on;
if (on && dirtyModeOn) lrSetDirtyMode(false);
if (emptyBtn) {
emptyBtn.textContent = on ? "Show all" : "Show empty";
emptyBtn.classList.toggle("lr-btn-dirty--active", on);
}
lrApplyFilter(currentFilter);
}
if (expandBtn) {
expandBtn.addEventListener("click", function() {
document.querySelectorAll(".lr-box, .lr-root-group").forEach(function(el) {
if (el.style.display !== "none") el.open = true;
});
});
}
if (collapseBtn) {
collapseBtn.addEventListener("click", function() {
document.querySelectorAll(".lr-box, .lr-root-group").forEach(function(el) {
if (el.style.display !== "none") el.open = false;
});
});
}
if (dirtyBtn) {
dirtyBtn.addEventListener("click", function() {
lrSetDirtyMode(!dirtyModeOn);
});
}
if (emptyBtn) {
emptyBtn.addEventListener("click", function() {
lrSetEmptyMode(!emptyModeOn);
});
}
document.querySelectorAll(".lr-textarea").forEach(function(ta) {
ta.addEventListener("input", lrUpdateDirty);
});
function lrApplyFilter(v) {
document.querySelectorAll(".lr-field").forEach(function(el) {
if (dirtyModeOn && !el.classList.contains("lr-field--dirty")) {
el.style.display = "none";
return;
}
if (emptyModeOn) {
var ta = el.querySelector(".lr-textarea");
if (ta && ta.value.trim() !== "") {
el.style.display = "none";
return;
}
}
el.style.display = ( !v || el.dataset.search.includes(v) ) ? "" : "none";
});
document.querySelectorAll(".lr-box").forEach(function(box) {
var visible = Array.from(box.querySelectorAll(".lr-field"))
.filter(function(el) { return el.style.display !== "none"; }).length;
box.style.display = visible ? "" : "none";
if (visible && v) box.open = true;
});
document.querySelectorAll(".lr-root-group").forEach(function(group) {
var visible = Array.from(group.querySelectorAll(".lr-field"))
.filter(function(el) { return el.style.display !== "none"; }).length;
group.style.display = visible ? "" : "none";
if (visible && v) {
group.open = true;
} else if (!v) {
group.open = group.dataset.initialOpen === "1";
}
});
lrUpdateCounts();
}
function lrIndexFields() {
document.querySelectorAll(".lr-field").forEach(function(el) {
el.dataset.search = el.textContent.toLowerCase();
});
document.querySelectorAll(".lr-root-group").forEach(function(group) {
if (!group.hasAttribute("data-initial-open")) {
group.dataset.initialOpen = group.open ? "1" : "0";
}
});
}
lrIndexFields();
setTimeout(lrIndexFields, 300);
lrUpdateCounts();
if (filterEl) {
filterEl.addEventListener("input", function() {
currentFilter = this.value.toLowerCase();
lrApplyFilter(currentFilter);
});
}
});
</script>';
}
/* =============================================================================
SETTINGS PAGE STYLES
============================================================================= */
function lr_acf_quick_settings_styles(): void {
echo '<style>
.lr-settings-wrap { max-width: 780px; }
.lr-settings-wrap h1 { margin-bottom: 20px; }
.lr-section {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 3px;
padding: 18px 22px;
margin-bottom: 16px;
}
.lr-section-title {
font-size: 13px;
font-weight: 700;
margin: 0 0 8px;
padding: 0 0 8px;
border-bottom: 1px solid #f0f0f0;
}
.lr-section .description { margin: 0 0 12px; color: #666; }
.lr-section input[type="text"] { margin-top: 4px; }
.lr-checks {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
margin-top: 6px;
}
.lr-checks--stack { flex-direction: column; gap: 10px; }
.lr-check-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
cursor: pointer;
}
.lr-check-label code {
font-size: 11px;
color: #888;
background: #f6f7f7;
padding: 1px 5px;
border-radius: 2px;
}
.lr-check-label em.lr-hint { font-size: 11px; color: #888; font-style: normal; }
.lr-indent {
margin-left: 22px;
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.lr-form-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.lr-btn-reset { color: #b32d2e !important; }
.lr-btn-reset:hover { border-color: #b32d2e !important; }
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
var toggle = document.getElementById("lr_root_enabled");
var wrap = document.getElementById("lr_root_placement_wrap");
if (toggle && wrap) {
toggle.addEventListener("change", function() {
wrap.style.display = this.checked ? "" : "none";
});
}
});
</script>';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment