*/
class Accessibility_Onetap_Settings_Manager {
/**
* Destination the "PRO" badges link to.
*
* The pricing page, which is where every other upgrade call to action on
* these screens already goes. A badge marks a feature the visitor cannot
* reach, so the plan that unlocks it is the answer to the click; the
* marketing home page is a detour.
*
* @var string
*/
const PRO_BADGE_URL = 'https://wponetap.com/pricing/?utm_source=dashboard-link&utm_medium=link&utm_campaign=ref-link-toolbar';
/**
* Slug AltPilot carries on the WordPress.org plugin repository.
*
* @var string
*/
const ALTPILOT_SLUG = 'altpilot';
/**
* Marketing site used whenever AltPilot cannot be installed from here.
*
* @var string
*/
const ALTPILOT_SITE_URL = 'https://www.altpilot.ai/';
/**
* Settings sections array.
*
* @var array
*/
protected $settings_sections = array();
/**
* Settings fields array.
*
* @var array
*/
protected $settings_fields = array();
/**
* Initialize __construct.
*/
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
}
/**
* Enqueue scripts and styles for the admin area.
*
* This function ensures that necessary scripts and styles are loaded on
* specific admin pages of the plugin. It loads the WordPress color picker,
* media uploader, jQuery, and a custom JavaScript file for managing settings.
*
* @param string $hook The current admin page hook suffix.
*/
public function admin_enqueue_scripts( $hook ) {
// Enqueue the WordPress color picker style.
wp_enqueue_style( 'wp-color-picker' );
// Enqueue the media uploader script.
wp_enqueue_media();
// Enqueue the WordPress color picker script.
wp_enqueue_script( 'wp-color-picker' );
// Enqueue the jQuery library.
wp_enqueue_script( 'jquery' );
// Conditionally enqueue the settings manager script for specific plugin admin pages.
if ( 'toplevel_page_accessibility-onetap-settings' === $hook ||
'onetap_page_accessibility-onetap-general-settings' === $hook ||
'onetap_page_accessibility-onetap-alt-text' === $hook ||
'onetap_page_accessibility-onetap-modules' === $hook ||
'admin_page_onetap-module-labels' === $hook ||
'onetap_page_accessibility-onetap-accessibility-status' === $hook
) {
wp_enqueue_script( 'accessibility-onetap-settings-manager', ACCESSIBILITY_ONETAP_PLUGINS_URL . 'assets/js/settings-manager.min.js', array( 'jquery' ), ACCESSIBILITY_ONETAP_VERSION, true );
}
}
/**
* Set settings sections.
*
* @param array $sections setting sections array.
*/
public function set_sections( $sections ) {
$this->settings_sections = $sections;
return $this;
}
/**
* Add a single section.
*
* @param array $section The section details.
*/
public function add_section( $section ) {
$this->settings_sections[] = $section;
return $this;
}
/**
* Set settings fields.
*
* @param array $fields settings fields array.
*/
public function set_fields( $fields ) {
$this->settings_fields = $fields;
return $this;
}
/**
* Add settings fields.
*
* @param array $section settings section array.
* @param array $field settings fields array.
*/
public function add_field( $section, $field ) {
$defaults = array(
'name' => '',
'label' => '',
'desc' => '',
'type' => 'text',
);
$arg = wp_parse_args( $field, $defaults );
$this->settings_fields[ $section ][] = $arg;
return $this;
}
/**
* Initialize and registers the settings sections and fileds to WordPress.
*
* Usually this should be called at `admin_init` hook.
*
* This function gets the initiated settings sections and fields. Then
* registers them to WordPress and ready for use.
*/
public function admin_init() {
// register settings sections.
foreach ( $this->settings_sections as $section ) {
if ( false === get_option( $section['id'] ) ) {
add_option( $section['id'] );
}
if ( isset( $section['desc'] ) && ! empty( $section['desc'] ) ) {
$section['desc'] = '
';
$this->render_template( $args, $input_html, 'number-slider' );
}
/**
* Callback function for rendering text input template in settings.
*
* This method generates HTML for a text input field with customizable attributes
* and optional save button. It supports various styling options and accessibility features.
*
* @param array $args settings field args.
*/
public function callback_template_text( $args ) {
$parsed_args = $this->parse_template_args( $args );
$parsed_args['type'] = isset( $args['type'] ) ? $args['type'] : 'text';
$parsed_args['classes'] = 'setting-control' . $parsed_args['first_control'] . ' ' . $parsed_args['last_control'] . ' ' . esc_attr( $parsed_args['type'] );
// Build input attributes.
$placeholder = ! empty( $args['placeholder'] ) ? 'placeholder="' . esc_attr( $args['placeholder'] ) . '"' : '';
// Build input HTML.
$input_html = '
';
$input_html .= '';
$input_html .= '
';
$this->render_template( $args, $input_html, 'text' );
}
/**
* Render a title and description section for settings forms.
*
* Outputs a settings section with optional title, description, label, and save button.
* This template is used for displaying informational content or section headers
* in the settings page without any input fields.
*
* @param array $args settings field args.
*/
public function callback_template_title_and_description( $args ) {
$this->render_template( $args, '', 'title-and-description' );
}
/**
* Render a device preview selector for settings forms.
*
* Outputs a device preview section with desktop, tablet, and mobile options.
* Allows users to preview how the accessibility widget will appear on different devices.
* Includes SVG icons for each device type and optional save button.
*
* @param array $args settings field args.
*/
public function callback_template_devices_tabs( $args ) {
// Build devices HTML.
$devices_html = '
';
$this->render_template( $args, $input_html, 'module-labels' );
}
/**
* Render a select dropdown field for settings forms.
*
* Outputs a select dropdown with predefined options from the select_options array.
* Includes optional title, description, label, and save button.
* The select field allows users to choose from a list of predefined options.
*
* @param array $args settings field args.
*/
public function callback_template_select( $args ) {
$parsed_args = $this->parse_template_args( $args );
$select_options = isset( $args['select_options'] ) ? $args['select_options'] : array();
$select_badge = isset( $args['select_badge'] ) ? $args['select_badge'] : '';
// Build select HTML.
$select_html = '
';
// Trigger showing the selected tags. Locked PRO controls have no script
// behind them, so the button is disabled rather than left focusable and
// inert — the overlay alone would not keep it out of the tab order.
$html .= '';
// Popup with search field and option list.
$html .= '
'; // .language-select-wrapper
// Get saved language toggle settings.
$onetap_settings = get_option( 'onetap_settings', array() );
$language_toggles = isset( $onetap_settings['toggle-language'] ) && is_array( $onetap_settings['toggle-language'] ) ? $onetap_settings['toggle-language'] : array();
// Check if toggle-language key exists (if not, means first time, default all ON).
$is_first_time = ! isset( $onetap_settings['toggle-language'] );
// If first time, set default all languages as ON (but don't save yet to avoid issues during rendering).
if ( $is_first_time && ! empty( $select_options ) ) {
$default_toggles = array();
foreach ( $select_options as $key => $label ) {
$default_toggles[ $key ] = 'on';
}
$language_toggles = $default_toggles;
}
// Dropdown options list.
$dropdown_html .= '
';
foreach ( $select_options as $key => $label ) {
$is_selected = ( $current_value === $key );
$flag_image = isset( $flag_images[ $key ] ) ? $flag_images[ $key ] : 'english.png';
$toggle_id = 'onetap_settings[toggle-language-' . esc_attr( $key ) . ']';
$toggle_name = 'onetap_settings[toggle-language][' . esc_attr( $key ) . ']';
// Whether this language is offered, decided by the same rule the
// toolbar reads. A language the admin has never been shown - one a
// plugin update added - appears here switched off, waiting to be
// chosen, rather than turning itself on across their site.
$is_toggle_on = $is_first_time || Accessibility_Onetap_Config::is_language_enabled( $key, $language_toggles );
$checked_attr = $is_toggle_on ? ' checked="checked"' : '';
$dropdown_html .= '
'; // End row.
}
} else {
$empty_msg = 'all' === $alt_filter
? __( 'No images found in Media Library.', 'accessibility-onetap' )
: __( 'No images match this filter.', 'accessibility-onetap' );
$empty_hint = 'all' === $alt_filter
? __( 'Upload images to your Media Library to start managing their alt text here.', 'accessibility-onetap' )
: __( 'Try choosing a different filter to see more of your images.', 'accessibility-onetap' );
$empty_image = ACCESSIBILITY_ONETAP_PLUGINS_URL . 'assets/images/admin/no-images-placeholder.svg';
$list_html .= '
';
$list_html .= '
';
$list_html .= '
';
$list_html .= '';
$list_html .= '
';
$list_html .= '
' . esc_html( $empty_msg ) . '
';
$list_html .= '
' . esc_html( $empty_hint ) . '
';
$list_html .= '
';
$list_html .= '
';
}
$list_html .= '
'; // End table-body.
$list_html .= '
'; // End box-image-alt.
// Pagination.
if ( $total_pages > 1 ) {
$list_html .= '
';
return $html;
}
/**
* Sanitize callback for Settings API.
*
* @param object $options sanitize options.
*/
public function sanitize_options( $options ) {
if ( ! $options ) {
$options = array();
}
// Handle toggle-language array: ensure all available languages are included.
$all_languages = $this->get_all_available_languages();
if ( ! empty( $all_languages ) ) {
// Get existing settings.
$existing_settings = get_option( 'onetap_settings', array() );
$existing_toggles = isset( $existing_settings['toggle-language'] ) && is_array( $existing_settings['toggle-language'] )
? $existing_settings['toggle-language']
: array();
// Get toggle-language from form submission (if any).
// Checkboxes that are unchecked are not sent in form submission, so we need to handle this.
$form_toggles = isset( $options['toggle-language'] ) && is_array( $options['toggle-language'] )
? $options['toggle-language']
: array();
// Check if this is first time (no existing data).
$is_first_time = empty( $existing_toggles );
// Check if form was submitted with toggle-language data.
// If form_toggles is not empty, it means user interacted with language toggles.
$has_toggle_submission = ! empty( $form_toggles );
// Process toggle-language based on different scenarios.
$merged_toggles = array();
// SCENARIO ANALYSIS:.
// 1. Data kosong / Fresh install: Default all 'on'.
// 2. Data sebelumnya ada + form tanpa toggle data: Preserve existing.
// 3. Form dengan toggle data (Enable All): All checked = all 'on'.
// 4. Form dengan toggle data (Disable All): Only default checked = others 'off'.
// 5. Form dengan toggle data (Enable Some): Checked = 'on', unchecked = 'off'.
foreach ( $all_languages as $lang_code ) {
if ( isset( $form_toggles[ $lang_code ] ) ) {
// SCENARIO: Checkbox is checked (in form submission).
// Cases: Enable All, Enable Some, etc.
$sanitized_value = sanitize_text_field( $form_toggles[ $lang_code ] );
$merged_toggles[ $lang_code ] = ( 'on' === $sanitized_value ) ? 'on' : 'off';
} elseif ( $has_toggle_submission ) {
// SCENARIO: Form was submitted with toggle data, but this checkbox is NOT in it (was unchecked).
// Cases: Disable All, Enable Some (where this one is off), etc.
// When user interacts with toggles and this one is not checked, it means 'off'.
$merged_toggles[ $lang_code ] = 'off';
} elseif ( ! $is_first_time ) {
// SCENARIO: Not first time, but form was submitted WITHOUT toggle-language data.
// This means user submitted form but didn't interact with language toggles.
// Preserve existing value (don't change anything).
$merged_toggles[ $lang_code ] = isset( $existing_toggles[ $lang_code ] ) && 'on' === $existing_toggles[ $lang_code ] ? 'on' : 'off';
} else {
// SCENARIO: First time, no toggle submission, no existing data.
// Default to 'on' for initial display.
$merged_toggles[ $lang_code ] = 'on';
}
}
// Safety check: If first time and no toggle submission, ensure all are set to 'on' (default).
// This handles: Data kosong, Fresh install scenarios.
if ( $is_first_time && ! $has_toggle_submission ) {
foreach ( $all_languages as $lang_code ) {
if ( ! isset( $merged_toggles[ $lang_code ] ) ) {
$merged_toggles[ $lang_code ] = 'on';
}
}
}
$options['toggle-language'] = $merged_toggles;
} elseif ( isset( $existing_settings['toggle-language'] ) && is_array( $existing_settings['toggle-language'] ) && ! isset( $options['toggle-language'] ) ) {
// If no languages found, preserve existing toggle-language if it exists.
$options['toggle-language'] = $existing_settings['toggle-language'];
}
// Get all registered switch fields to check for missing checkboxes.
$all_switch_fields = array();
foreach ( $this->settings_fields as $section => $section_options ) {
foreach ( $section_options as $option ) {
if ( isset( $option['type'] ) && 'switch' === $option['type'] ) {
$all_switch_fields[ $option['name'] ] = $option;
}
}
}
// Handle missing checkbox values (set to 'off' if not present).
foreach ( $all_switch_fields as $field_name => $field_config ) {
if ( ! isset( $options[ $field_name ] ) ) {
$options[ $field_name ] = 'off';
}
}
foreach ( $options as $option_slug => $option_value ) {
$sanitize_callback = $this->get_sanitize_callback( $option_slug );
// If callback is set, call it.
if ( $sanitize_callback ) {
$options[ $option_slug ] = call_user_func( $sanitize_callback, $option_value );
continue;
}
}
return $options;
}
/**
* Get sanitization callback for given option slug.
*
* @param string $slug option slug.
*
* @return mixed string or bool false.
*/
public function get_sanitize_callback( $slug = '' ) {
if ( empty( $slug ) ) {
return false;
}
// Iterate over registered fields and see if we can find proper callback.
foreach ( $this->settings_fields as $section => $options ) {
foreach ( $options as $option ) {
if ( $option['name'] !== $slug ) {
continue;
}
// Return the callback name.
return isset( $option['sanitize_callback'] ) && is_callable( $option['sanitize_callback'] ) ? $option['sanitize_callback'] : false;
}
}
return false;
}
/**
* Get the value of a settings field.
*
* @param string $option settings field name.
* @param string $section the section name this field belongs to.
* @param string $default_value default value text if it's not found.
* @return string
*/
public function get_option( $option, $section, $default_value = '' ) {
$options = get_option( $section );
if ( isset( $options[ $option ] ) ) {
return $options[ $option ];
}
// For switch/checkbox type fields, return 'off' as default if no default is specified.
if ( empty( $default_value ) ) {
// Check if this is a switch field.
foreach ( $this->settings_fields as $section_name => $section_options ) {
foreach ( $section_options as $field ) {
if ( $field['name'] === $option && isset( $field['type'] ) && 'switch' === $field['type'] ) {
return 'off';
}
}
}
}
return $default_value;
}
/**
* Show navigations as tab.
*
* Shows all the settings section labels as tab.
*/
public function show_navigation() {
$html = '
';
$count = count( $this->settings_sections );
// Don't show the navigation if only one section exists.
if ( 1 === $count ) {
return;
}
foreach ( $this->settings_sections as $tab ) {
$html .= '' . esc_html( $tab['title'] ) . '';
}
$html .= '
';
echo wp_kses(
$html,
array(
'h2' => array(),
'a' => array(
'href' => array(),
'class' => array(),
'id' => array(),
),
)
);
}
/**
* Show the section settings forms.
*
* This function displays every sections in a different form.
*/
public function show_forms() {
?>
settings_sections as $form ) { ?>
settings_fields as $section => $section_options ) {
foreach ( $section_options as $option ) {
if ( isset( $option['name'] ) && 'language' === $option['name'] && isset( $option['select_options'] ) && is_array( $option['select_options'] ) ) {
$all_languages = array_keys( $option['select_options'] );
break 2; // Break out of both loops.
}
}
}
// Fallback: if settings_fields not yet initialized, get from options class directly.
if ( empty( $all_languages ) ) {
// Try to get from Onetap_Pro_Accessibility_Settings_Options if available.
if ( class_exists( 'Onetap_Pro_Accessibility_Settings_Options' ) ) {
$options_instance = new Onetap_Pro_Accessibility_Settings_Options();
$fields = $options_instance->get_settings_api_fields();
// Find language field.
if ( isset( $fields['onetap_settings'] ) ) {
foreach ( $fields['onetap_settings'] as $field ) {
if ( isset( $field['name'] ) && 'language' === $field['name'] && isset( $field['select_options'] ) && is_array( $field['select_options'] ) ) {
$all_languages = array_keys( $field['select_options'] );
break;
}
}
}
}
}
return $all_languages;
}
}