PHP
<?php
/**
* Field Framework for WordPress
* Handles theme options, post meta, and term meta fields
*/
class WP_Field_Framework {
private static $instance = null;
private $fields = [];
private $sections = [];
private $options = [];
private $page_slug = 'theme-options';
private $option_group = 'theme_options';
public static function get_instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function __construct() {
add_action('admin_menu', [$this, 'add_options_page']);
add_action('admin_init', [$this, 'register_settings']);
add_action('add_meta_boxes', [$this, 'register_meta_boxes']);
add_action('created_term', [$this, 'save_term_fields'], 10, 3);
add_action('edit_term', [$this, 'save_term_fields'], 10, 3);
add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
add_action('save_post', [$this, 'save_post_meta']); // Add this line
// Add term meta field actions
add_action('admin_init', [$this, 'register_term_fields']);
}
/**
* Enqueue scripts and styles
*/
public function enqueue_scripts($hook) {
// Only load on our settings page or term/post edit screens
if ($hook === 'settings_page_' . $this->page_slug ||
$hook === 'post.php' ||
$hook === 'post-new.php' ||
$hook === 'term.php' ||
$hook === 'edit-tags.php') {
wp_enqueue_media();
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
// Add custom scripts
wp_enqueue_script(
'field-framework',
get_stylesheet_directory_uri() . '/js/field-framework.js',
['jquery', 'wp-color-picker'],
'1.0.0',
true
);
}
}
/**
* Add options page to admin menu
*/
public function add_options_page() {
add_options_page(
'Theme Options',
'Theme Options',
'manage_options',
$this->page_slug,
[$this, 'render_options_page']
);
}
/**
* Add a new section
*/
public function add_section($args) {
$defaults = [
'id' => '',
'title' => '',
'description' => '',
'icon' => '', // Dashicons class
'priority' => 10
];
$section = wp_parse_args($args, $defaults);
$this->sections[$section['id']] = $section;
return $this;
}
/**
* Add a new field
*/
public function add_field($args) {
$defaults = [
'id' => '',
'type' => 'text',
'title' => '',
'description' => '',
'default' => '',
'options' => [],
'context' => 'theme_option',
'post_type' => 'post',
'taxonomy' => '',
'section' => 'general' // Default section
];
$field = wp_parse_args($args, $defaults);
$this->fields[] = $field;
return $this;
}
/**
* Sanitize field value based on type
*/
private function sanitize_field_value($value, $type) {
switch ($type) {
case 'text':
return sanitize_text_field($value);
case 'textarea':
return sanitize_textarea_field($value);
case 'checkbox':
return ($value == '1') ? '1' : '0';
case 'select':
return sanitize_text_field($value);
case 'color':
return sanitize_hex_color($value);
case 'image':
return absint($value);
default:
return sanitize_text_field($value);
}
}
/**
* Register settings and sections
*/
public function register_settings() {
// Register setting
register_setting(
$this->option_group,
$this->option_group,
[$this, 'sanitize_options']
);
// Sort sections by priority
uasort($this->sections, function($a, $b) {
return $a['priority'] - $b['priority'];
});
// If no sections defined, add default
if (empty($this->sections)) {
$this->sections['general'] = [
'id' => 'general',
'title' => 'General Settings',
'description' => '',
'priority' => 10
];
}
// Get current tab
$current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : array_key_first($this->sections);
// Add only the current section
if (isset($this->sections[$current_tab])) {
add_settings_section(
$current_tab,
$this->sections[$current_tab]['title'],
[$this, 'section_callback'],
$this->page_slug
);
}
// Add only fields for the current section
foreach ($this->fields as $field) {
if ($field['context'] === 'theme_option' && $field['section'] === $current_tab) {
add_settings_field(
$field['id'],
$field['title'],
[$this, 'render_field'],
$this->page_slug,
$current_tab,
$field
);
}
}
}
/**
* Section callback
*/
public function section_callback($args) {
$section_id = $args['id'];
if (isset($this->sections[$section_id]['description'])) {
echo '<p class="description">' . esc_html($this->sections[$section_id]['description']) . '</p>';
}
}
/**
* Sanitize options
*/
public function sanitize_options($options) {
$sanitized = [];
// Get existing options to preserve other tab values
$existing_options = get_option($this->option_group, []);
$sanitized = $existing_options;
foreach ($this->fields as $field) {
if (isset($options[$field['id']])) {
$sanitized[$field['id']] = $this->sanitize_field_value($options[$field['id']], $field['type']);
}
}
// Add redirect to current tab after save
if (isset($options[$this->option_group . '_current_tab'])) {
add_settings_error(
'general',
'settings_updated',
__('Settings saved.'),
'updated'
);
wp_redirect(add_query_arg([
'page' => $this->page_slug,
'tab' => $options[$this->option_group . '_current_tab']
], admin_url('options-general.php')));
exit;
}
return $sanitized;
}
/**
* Render options page
*/
public function render_options_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.'));
}
// Get current tab
$current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : array_key_first($this->sections);
// Store current tab in hidden field for post-save redirect
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<nav class="nav-tab-wrapper">
<?php foreach ($this->sections as $section_id => $section) : ?>
<?php
$active_class = ($current_tab === $section_id) ? 'nav-tab-active' : '';
$icon = !empty($section['icon']) ? '<span class="dashicons ' . esc_attr($section['icon']) . '"></span> ' : '';
?>
<a href="?page=<?php echo $this->page_slug; ?>&tab=<?php echo esc_attr($section_id); ?>"
class="nav-tab <?php echo $active_class; ?>">
<?php echo wp_kses_post($icon . $section['title']); ?>
</a>
<?php endforeach; ?>
</nav>
<form action="options.php" method="post">
<input type="hidden" name="<?php echo $this->option_group; ?>_current_tab" value="<?php echo esc_attr($current_tab); ?>">
<?php
settings_fields($this->option_group);
do_settings_sections($this->page_slug);
submit_button('Save Settings');
?>
</form>
</div>
<style>
.nav-tab .dashicons {
line-height: 1.35;
}
.form-table th {
padding: 20px 10px;
}
</style>
<?php
}
/**
* Register meta boxes
*/
public function register_meta_boxes() {
foreach ($this->fields as $field) {
if ($field['context'] === 'post_meta') {
add_meta_box(
$field['id'],
$field['title'],
[$this, 'render_meta_box'],
$field['post_type'],
'normal',
'default',
['field' => $field]
);
}
}
}
/**
* Get field value based on context
*/
private function get_field_value($field, $object = null) {
switch ($field['context']) {
case 'theme_option':
$options = get_option($this->option_group, []);
return isset($options[$field['id']]) ? $options[$field['id']] : $field['default'];
case 'post_meta':
$post_id = $object instanceof WP_Post ? $object->ID : get_the_ID();
return get_post_meta($post_id, $field['id'], true) ?: $field['default'];
case 'term_meta':
if ($object instanceof WP_Term) {
return get_term_meta($object->term_id, $field['id'], true) ?: $field['default'];
}
return $field['default'];
default:
return $field['default'];
}
}
/**
* Get field name based on context
*/
private function get_field_name($field) {
switch ($field['context']) {
case 'theme_option':
return $this->option_group . '[' . $field['id'] . ']';
default:
return $field['id'];
}
}
/**
* Render image field
*/
private function render_image_field($id, $name, $value) {
$image_url = wp_get_attachment_url($value);
?>
<div class="image-field-wrapper">
<input type="hidden" id="<?php echo $id; ?>" name="<?php echo $name; ?>" value="<?php echo esc_attr($value); ?>">
<div class="image-preview" style="margin-bottom: 10px;">
<?php if ($image_url) : ?>
<img src="<?php echo esc_url($image_url); ?>" style="max-width: 200px;">
<?php endif; ?>
</div>
<button type="button" class="button select-image"><?php _e('Select Image'); ?></button>
<button type="button" class="button remove-image" <?php echo empty($value) ? 'style="display:none;"' : ''; ?>>
<?php _e('Remove Image'); ?>
</button>
</div>
<?php
}
/**
* Unified field renderer
*/
public function render_field($field, $object = null) {
// Get the field value based on context
$value = $this->get_field_value($field, $object);
// Setup field name and ID
$id = esc_attr($field['id']);
$name = $this->get_field_name($field);
// Render the appropriate field type
switch ($field['type']) {
case 'text':
printf(
'<input type="text" id="%s" name="%s" value="%s" class="regular-text">',
$id,
$name,
esc_attr($value)
);
break;
case 'textarea':
printf(
'<textarea id="%s" name="%s" rows="5" class="large-text">%s</textarea>',
$id,
$name,
esc_textarea($value)
);
break;
case 'select':
echo '<select id="' . $id . '" name="' . $name . '">';
foreach ($field['options'] as $key => $label) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr($key),
selected($value, $key, false),
esc_html($label)
);
}
echo '</select>';
break;
case 'checkbox':
printf(
'<input type="checkbox" id="%s" name="%s" value="1" %s>',
$id,
$name,
checked($value, '1', false)
);
break;
case 'color':
printf(
'<input type="text" id="%s" name="%s" value="%s" class="color-picker">',
$id,
$name,
esc_attr($value)
);
break;
case 'image':
$this->render_image_field($id, $name, $value);
break;
}
// Show description if exists
if (!empty($field['description'])) {
printf(
'<p class="description">%s</p>',
esc_html($field['description'])
);
}
}
/**
* Render meta box
*/
public function render_meta_box($post, $args) {
$field = $args['args']['field'];
$value = get_post_meta($post->ID, $field['id'], true);
// Create nonce with consistent naming
wp_nonce_field('save_' . $field['id'], $field['id'] . '_nonce');
$this->render_field($field, $post);
}
/**
* Save post meta
*/
public function save_post_meta($post_id) {
// Basic security checks
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Save each field
foreach ($this->fields as $field) {
if ($field['context'] === 'post_meta') {
// Verify nonce
$nonce_name = $field['id'] . '_nonce';
if (!isset($_POST[$nonce_name]) ||
!wp_verify_nonce($_POST[$nonce_name], 'save_' . $field['id'])) {
continue;
}
// Handle checkbox fields specifically
if ($field['type'] === 'checkbox') {
$value = isset($_POST[$field['id']]) ? '1' : '0';
} else {
$value = isset($_POST[$field['id']]) ? $_POST[$field['id']] : '';
}
// Sanitize and save the value
$sanitized_value = $this->sanitize_field_value($value, $field['type']);
update_post_meta($post_id, $field['id'], $sanitized_value);
}
}
}
/**
* Register term fields
*/
public function register_term_fields() {
foreach ($this->fields as $field) {
if ($field['context'] === 'term_meta' && !empty($field['taxonomy'])) {
// Add field to term add form
add_action("{$field['taxonomy']}_add_form_fields", function() use ($field) {
// Wrap in div for add form styling
echo '<div class="form-field term-meta-wrap">';
echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
$this->render_field($field);
echo '</div>';
});
// Add field to term edit form
add_action("{$field['taxonomy']}_edit_form_fields", function($term) use ($field) {
echo '<tr class="form-field term-meta-wrap">';
echo '<th scope="row">';
echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>';
echo '</th>';
echo '<td>';
$this->render_field($field, $term);
echo '</td>';
echo '</tr>';
});
// Save term meta
add_action("created_{$field['taxonomy']}", function($term_id) use ($field) {
$this->save_term_meta($term_id, $field);
});
add_action("edited_{$field['taxonomy']}", function($term_id) use ($field) {
$this->save_term_meta($term_id, $field);
});
}
}
}
/**
* Save term fields
*/
public function save_term_fields($term_id, $tt_id, $taxonomy) {
foreach ($this->fields as $field) {
if ($field['context'] === 'term_meta' && $field['taxonomy'] === $taxonomy) {
if (isset($_POST[$field['id']])) {
update_term_meta($term_id, $field['id'], sanitize_text_field($_POST[$field['id']]));
}
}
}
}
/**
* Save term meta
*/
private function save_term_meta($term_id, $field) {
if (isset($_POST[$field['id']])) {
$value = $_POST[$field['id']];
$sanitized_value = $this->sanitize_field_value($value, $field['type']);
update_term_meta($term_id, $field['id'], $sanitized_value);
}
}
/**
* Get field value
*/
public function get_value($field_id, $context = 'theme_option', $object_id = null) {
switch ($context) {
case 'theme_option':
$options = get_option($this->option_group, []);
return isset($options[$field_id]) ? $options[$field_id] : '';
case 'post_meta':
return get_post_meta($object_id, $field_id, true);
case 'term_meta':
return get_term_meta($object_id, $field_id, true);
default:
return null;
}
}
/**
* Get field by ID
*/
private function get_field_by_id($field_id) {
foreach ($this->fields as $field) {
if ($field['id'] === $field_id) {
return $field;
}
}
return null;
}
}
// Initialize field framework
function wp_field_framework() {
return WP_Field_Framework::get_instance();
}
JAVASCRIPT
jQuery(document).ready(function($) {
// Initialize color pickers
$('.color-picker').wpColorPicker();
// Image upload handling
var frame;
$('.select-image').on('click', function(e) {
e.preventDefault();
var button = $(this);
var wrapper = button.closest('.image-field-wrapper');
var input = wrapper.find('input[type="hidden"]');
var preview = wrapper.find('.image-preview');
// If frame exists, reuse it
if (frame) {
frame.open();
return;
}
// Create WP media frame
frame = wp.media({
title: 'Select or Upload Image',
button: {
text: 'Use this image'
},
multiple: false
});
// Handle image selection
frame.on('select', function() {
var attachment = frame.state().get('selection').first().toJSON();
input.val(attachment.id);
preview.html('<img src="' + attachment.url + '" style="max-width: 200px;">');
wrapper.find('.remove-image').show();
});
frame.open();
});
// Remove image
$('.remove-image').on('click', function(e) {
e.preventDefault();
var button = $(this);
var wrapper = button.closest('.image-field-wrapper');
var input = wrapper.find('input[type="hidden"]');
var preview = wrapper.find('.image-preview');
input.val('');
preview.empty();
button.hide();
});
});
用法
<?php
// Initialize the framework
$fields = wp_field_framework();
// Add sections (groups)
$fields->add_section([
'id' => 'header',
'title' => 'Header Settings',
'description' => 'Customize your site header',
'icon' => 'dashicons-heading', // WordPress dashicons class
'priority' => 10
]);
$fields->add_section([
'id' => 'homepage',
'title' => 'Homepage Settings',
'description' => 'Customize your homepage layout',
'icon' => 'dashicons-admin-home',
'priority' => 20
]);
$fields->add_section([
'id' => 'footer',
'title' => 'Footer Settings',
'description' => 'Customize your site footer',
'icon' => 'dashicons-arrow-down',
'priority' => 30
]);
// Add fields to specific sections
$fields->add_field([
'id' => 'header_logo',
'type' => 'image',
'title' => 'Logo',
'description' => 'Upload your site logo',
'section' => 'header'
]);
$fields->add_field([
'id' => 'homepage_hero_title',
'type' => 'text',
'title' => 'Hero Title',
'description' => 'Enter your homepage hero title',
'section' => 'homepage'
]);
$fields->add_field([
'id' => 'footer_copyright',
'type' => 'textarea',
'title' => 'Copyright Text',
'description' => 'Enter your footer copyright text',
'section' => 'footer'
]);
$fields->add_field([
'id' => 'footer_logo',
'type' => 'image',
'title' => 'category logo',
'description' => 'Enter your footer copyright text',
'context' => 'term_meta',
'taxonomy' => 'genre'
]);
$fields->add_field([
'id' => 'footer_logo',
'type' => 'image',
'title' => 'post logo',
'description' => 'Enter your footer copyright text',
'context' => 'post_meta',
'post_type' => 'book'
]);