mirror of
https://github.com/WPTechnix/wp-settings-framework.git
synced 2025-10-03 23:23:25 +08:00
Improved Settings Class Handling (#3)
feat(core)!: overhaul settings engine, fix data loss, and improve API This major release resolves a critical data loss bug and refactors the entire framework to be more robust, configurable, and developer-friendly. BREAKING CHANGE: The `Settings` class constructor signature has changed. It now requires `$optionName` and `$pageSlug` as the first two arguments. Page titles and menu titles must now be passed via the `$options` array to allow for better configuration. Example before: new Settings('My Page', 'my-menu', 'my-option', 'my-slug'); Example now: new Settings('my-option', 'my-slug', [ 'pageTitle' => 'My Page', 'menuTitle' => 'My Menu', ]); --- ### Key Fixes and Improvements: * **Critical Data Loss Resolved:** A bug that erased settings from inactive tabs during save has been fixed. The `Sanitizer` is now state-aware, merging new input with existing options to preserve all data. * **Reliable Data Retrieval:** The public `get()` method, which was previously non-functional, has been completely repaired. It now correctly fetches saved values from the database with proper fallbacks to field defaults. * **Enhanced Configuration & API:** - New fluent methods `setCapability()` and `setParentSlug()` have been added. - All text strings are now configurable via a `labels` array in the constructor, improving support for internationalization (i18n). - External asset libraries (like Select2) can now be configured. * **Internal Architecture:** These improvements were made possible by a complete internal refactor, which introduced a new `Config` class for state management and enforced proper dependency injection.
This commit is contained in:
parent
327ec7e9b2
commit
6507e672e7
41 changed files with 1943 additions and 1274 deletions
3
.github/SECURITY.md
vendored
3
.github/SECURITY.md
vendored
|
@ -1,5 +1,6 @@
|
|||
# Reporting a Vulnerability
|
||||
|
||||
If you discover a security-related issue, please report it privately by emailing **[security@wptechnix.com](mailto:security@wptechnix.com)**.
|
||||
If you discover a security-related issue, please report it privately by emailing *
|
||||
*[security@wptechnix.com](mailto:security@wptechnix.com)**.
|
||||
|
||||
Avoid disclosing security vulnerabilities publicly.
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: [8.0, 8.4]
|
||||
php-version: [ 8.0, 8.4 ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
|
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
|
@ -3,7 +3,7 @@ name: Lint Commit Messages
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
types: [ opened, edited, synchronize ]
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
|
|
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -2,33 +2,45 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
|
||||
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2025-08-11
|
||||
|
||||
This is the initial public release of the WPTechnix Settings Framework. This version represents a complete architectural overhaul from its internal, monolithic predecessor, refactoring it into a modern, decoupled, and highly extensible framework ready for public use.
|
||||
This is the initial public release of the WPTechnix Settings Framework. This version represents a complete architectural
|
||||
overhaul from its internal, monolithic predecessor, refactoring it into a modern, decoupled, and highly extensible
|
||||
framework ready for public use.
|
||||
|
||||
### Added
|
||||
|
||||
* **Complete Architectural Refactor:** The entire framework has been broken down into small, single-responsibility classes, following modern object-oriented design principles.
|
||||
* **Composer Support:** The framework is now fully PSR-4 compliant and installable as a standard Composer package (`wptechnix/wp-settings-framework`).
|
||||
* **Dependency Injection (DI) Friendly Design:**
|
||||
* Introduced a primary `Settings` class that acts as a cohesive manager for building a page and retrieving its data.
|
||||
* Added a `Interfaces\SettingsInterface` to allow for clean dependency injection and testability.
|
||||
* Removed all static accessors (`SettingsRegistry`) in favor of an object-oriented, injectable architecture.
|
||||
* **Extensible Field System:**
|
||||
* Created a `FieldFactory` and an `AbstractField` base class, making it simple to add new, custom field types.
|
||||
* All 20+ field types are now individual classes, making the system easier to maintain and extend.
|
||||
* **Dedicated Asset Management:** A new `AssetManager` class now handles all CSS and JavaScript enqueueing.
|
||||
* **Intelligent Asset Loading:**
|
||||
* The `AssetManager` automatically detects which field types are being used and only enqueues the necessary libraries (e.g., Select2, Flatpickr, CodeMirror modes).
|
||||
* Includes a fallback mechanism to prevent conflicts: it will not load a library from its CDN if a theme or another plugin has already registered it.
|
||||
* **Configurable HTML Class Prefixing:**
|
||||
* Introduced a new `htmlPrefix` option (defaults to `wptechnix-settings`) to prevent CSS and JS class name collisions in the WordPress admin.
|
||||
* This prefix is now consistently applied to all custom field elements and their containers.
|
||||
* **Flexible `Settings` Builder:**
|
||||
* The `pageTitle` and `menuTitle` are now optional in the `Settings` constructor, defaulting to WordPress's standard "Settings" text for simpler setups.
|
||||
* Added fluent setter methods (`setPageTitle()`, `setMenuTitle()`) for more flexible configuration.
|
||||
* **Enhanced Code Editor Field:** The `code` field now accepts a `language` parameter (`css`, `javascript`, `html`) to enable the correct syntax highlighting mode.
|
||||
* **Full PHPDoc Coverage:** Every class, method, and property is now fully documented according to PHPDoc and PSR-12 standards.
|
||||
* **Comprehensive `README.md` and `CHANGELOG.md`:** Added official documentation for installation, usage, and project history.
|
||||
* **Complete Architectural Refactor:** The entire framework has been broken down into small, single-responsibility
|
||||
classes, following modern object-oriented design principles.
|
||||
* **Composer Support:** The framework is now fully PSR-4 compliant and installable as a standard Composer package (
|
||||
`wptechnix/wp-settings-framework`).
|
||||
* **Dependency Injection (DI) Friendly Design:**
|
||||
* Introduced a primary `Settings` class that acts as a cohesive manager for building a page and retrieving its data.
|
||||
* Added a `Interfaces\SettingsInterface` to allow for clean dependency injection and testability.
|
||||
* Removed all static accessors (`SettingsRegistry`) in favor of an object-oriented, injectable architecture.
|
||||
* **Extensible Field System:**
|
||||
* Created a `FieldFactory` and an `AbstractField` base class, making it simple to add new, custom field types.
|
||||
* All 20+ field types are now individual classes, making the system easier to maintain and extend.
|
||||
* **Dedicated Asset Management:** A new `AssetManager` class now handles all CSS and JavaScript enqueueing.
|
||||
* **Intelligent Asset Loading:**
|
||||
* The `AssetManager` automatically detects which field types are being used and only enqueues the necessary
|
||||
libraries (e.g., Select2, Flatpickr, CodeMirror modes).
|
||||
* Includes a fallback mechanism to prevent conflicts: it will not load a library from its CDN if a theme or another
|
||||
plugin has already registered it.
|
||||
* **Configurable HTML Class Prefixing:**
|
||||
* Introduced a new `htmlPrefix` option (defaults to `wptechnix-settings`) to prevent CSS and JS class name
|
||||
collisions in the WordPress admin.
|
||||
* This prefix is now consistently applied to all custom field elements and their containers.
|
||||
* **Flexible `Settings` Builder:**
|
||||
* The `pageTitle` and `menuTitle` are now optional in the `Settings` constructor, defaulting to WordPress's
|
||||
standard "Settings" text for simpler setups.
|
||||
* Added fluent setter methods (`setPageTitle()`, `setMenuTitle()`) for more flexible configuration.
|
||||
* **Enhanced Code Editor Field:** The `code` field now accepts a `language` parameter (`css`, `javascript`, `html`) to
|
||||
enable the correct syntax highlighting mode.
|
||||
* **Full PHPDoc Coverage:** Every class, method, and property is now fully documented according to PHPDoc and PSR-12
|
||||
standards.
|
||||
* **Comprehensive `README.md` and `CHANGELOG.md`:** Added official documentation for installation, usage, and project
|
||||
history.
|
||||
|
|
155
README.md
155
README.md
|
@ -1,47 +1,55 @@
|
|||
# WPTechnix Settings Framework
|
||||
|
||||
A modern, object-oriented PHP framework for creating powerful and professional settings pages in WordPress. Designed with a clean, fluent API, this framework saves you time by abstracting away the complexities of the WordPress Settings API.
|
||||
A modern, object-oriented PHP framework for creating powerful and professional settings pages in WordPress. Designed
|
||||
with a clean, fluent API, this framework saves you time by abstracting away the complexities of the WordPress Settings
|
||||
API.
|
||||
|
||||
Build complex, tabbed settings pages with over 20 field types, conditional logic, and an enhanced user interface, all while storing your data efficiently in a single database option.
|
||||
Build complex, tabbed settings pages with over 20 field types, conditional logic, and an enhanced user interface, all
|
||||
while storing your data efficiently in a single database option.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
* **Fluent, Chainable API:** Build your entire settings page in a clean, readable, and intuitive way.
|
||||
* **Efficient Database Storage:** All settings are saved to a single array in the `wp_options` table, reducing database clutter and improving performance.
|
||||
* **Rich & Modern Field Types:** Includes over 20 field types, with enhanced UI for color pickers, media uploaders, date/time selectors, and more.
|
||||
* **Tabbed Interface:** Easily organize your settings into clean, separate tabs with support for Dashicons.
|
||||
* **Conditional Field Logic:** Show or hide fields based on the value of another field (e.g., show "Log File Path" only when "Enable Debugging" is toggled on).
|
||||
* **Code Editor Fields:** Includes a `code` field with syntax highlighting for CSS, JavaScript, and HTML, powered by the built-in WordPress CodeMirror library.
|
||||
* **Conflict-Free Prefixing:** All custom HTML classes for CSS and JS are prefixed to prevent conflicts with other plugins and themes. This prefix is fully configurable.
|
||||
* **Composer Ready:** Fully PSR-4 compliant and ready to be included as a dependency in any modern WordPress project.
|
||||
* **Fluent, Chainable API:** Build your entire settings page in a clean, readable, and intuitive way.
|
||||
* **Efficient Database Storage:** All settings are saved to a single array in the `wp_options` table, reducing database
|
||||
clutter and improving performance.
|
||||
* **Rich & Modern Field Types:** Includes over 20 field types, with enhanced UI for color pickers, media uploaders,
|
||||
date/time selectors, and more.
|
||||
* **Tabbed Interface:** Easily organize your settings into clean, separate tabs with support for Dashicons.
|
||||
* **Conditional Field Logic:** Show or hide fields based on the value of another field (e.g., show "Log File Path" only
|
||||
when "Enable Debugging" is toggled on).
|
||||
* **Code Editor Fields:** Includes a `code` field with syntax highlighting for CSS, JavaScript, and HTML, powered by the
|
||||
built-in WordPress CodeMirror library.
|
||||
* **Conflict-Free Prefixing:** All custom HTML classes for CSS and JS are prefixed to prevent conflicts with other
|
||||
plugins and themes. This prefix is fully configurable.
|
||||
* **Composer Ready:** Fully PSR-4 compliant and ready to be included as a dependency in any modern WordPress project.
|
||||
|
||||
## Available Field Types
|
||||
|
||||
The framework includes the following field types out of the box:
|
||||
|
||||
| Type | Description |
|
||||
| :--- | :--- |
|
||||
| `text` | A standard single-line text input. |
|
||||
| `email` | A text input with `type="email"` validation. |
|
||||
| `password` | A text input with `type="password"`. |
|
||||
| `number` | A number input with `type="number"`. |
|
||||
| `url` | A text input with `type="url"` validation. |
|
||||
| `textarea` | A standard multi-line text area. |
|
||||
| `checkbox` | A single checkbox. |
|
||||
| `toggle` | An on/off toggle switch (saves a boolean). |
|
||||
| `select` | A dropdown select menu. |
|
||||
| `multiselect` | A multi-select dropdown menu. |
|
||||
| `radio` | A group of radio buttons. |
|
||||
| `buttongroup` | A modern button group that functions like a radio input. |
|
||||
| `color` | A color picker field. |
|
||||
| `date` | A date picker. |
|
||||
| `datetime` | A date and time picker. |
|
||||
| `time` | A time picker. |
|
||||
| `range` | An enhanced range slider with a value display. |
|
||||
| `media` | A media uploader that uses the WordPress Media Library. |
|
||||
| `code` | A code editor with syntax highlighting. |
|
||||
| Type | Description |
|
||||
|:--------------|:--------------------------------------------------------------|
|
||||
| `text` | A standard single-line text input. |
|
||||
| `email` | A text input with `type="email"` validation. |
|
||||
| `password` | A text input with `type="password"`. |
|
||||
| `number` | A number input with `type="number"`. |
|
||||
| `url` | A text input with `type="url"` validation. |
|
||||
| `textarea` | A standard multi-line text area. |
|
||||
| `checkbox` | A single checkbox. |
|
||||
| `toggle` | An on/off toggle switch (saves a boolean). |
|
||||
| `select` | A dropdown select menu. |
|
||||
| `multiselect` | A multi-select dropdown menu. |
|
||||
| `radio` | A group of radio buttons. |
|
||||
| `buttongroup` | A modern button group that functions like a radio input. |
|
||||
| `color` | A color picker field. |
|
||||
| `date` | A date picker. |
|
||||
| `datetime` | A date and time picker. |
|
||||
| `time` | A time picker. |
|
||||
| `range` | An enhanced range slider with a value display. |
|
||||
| `media` | A media uploader that uses the WordPress Media Library. |
|
||||
| `code` | A code editor with syntax highlighting. |
|
||||
| `description` | A read-only field used to display text, lists, or other HTML. |
|
||||
|
||||
## Installation
|
||||
|
@ -49,46 +57,58 @@ The framework includes the following field types out of the box:
|
|||
This package is intended to be used as a Composer dependency.
|
||||
|
||||
Install the package via the command line:
|
||||
|
||||
```bash
|
||||
composer require wptechnix/wp-settings-framework
|
||||
```
|
||||
|
||||
Make sure your project's `vendor/autoload.php` file is included to autoload the framework's classes.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Creating a settings page is simple. In your plugin's main bootstrap file or a dedicated service class, instantiate the `\WPTechnix\WPSettings\Settings` class and use its fluent methods to build your page.
|
||||
Creating a settings page is simple. In your plugin's main bootstrap file or a dedicated service class, instantiate the
|
||||
`\WPTechnix\WPSettings\Settings` class and use its fluent methods to build your page.
|
||||
|
||||
### Example Usage
|
||||
|
||||
Here is a complete example of how to build a tabbed settings page for a fictional "My Awesome Plugin".
|
||||
Here is a complete example of how to build a tabbed settings page for a fictional "My Awesome Plugin". This example also
|
||||
demonstrates the correct way to handle translations.
|
||||
|
||||
```php
|
||||
<?php
|
||||
// In your plugin's main file or a class that runs on `plugins_loaded`.
|
||||
|
||||
use WPTechnix\WPSettings\Settings;
|
||||
use WPTechnix\WPSettings\Contracts\SettingsInterface;
|
||||
// This line is only needed if you are outside a namespace.
|
||||
// use WPTechnix\WPSettings\Settings;
|
||||
|
||||
add_action('plugins_loaded', function () {
|
||||
|
||||
// Define your text domain for translations.
|
||||
$text_domain = 'my-awesome-plugin';
|
||||
|
||||
// 1. Create a new Settings instance.
|
||||
// The only required argument is a unique slug for your page.
|
||||
$settingsManager = new Settings(
|
||||
'my-awesome-plugin',
|
||||
'My Awesome Plugin Settings', // Page Title
|
||||
'Awesome Plugin' // Menu Title
|
||||
// The first two arguments are required: a unique option name and a unique page slug.
|
||||
$settingsManager = new \WPTechnix\WPSettings\Settings(
|
||||
'my_awesome_plugin_options', // Unique option name to store in DB
|
||||
'my-awesome-plugin-settings', // Unique page slug
|
||||
[
|
||||
// Provide your own translated strings to override the defaults.
|
||||
// This allows translation tools to correctly scan your plugin.
|
||||
'pageTitle' => __('My Awesome Plugin Settings', $text_domain),
|
||||
'menuTitle' => __('Awesome Plugin', $text_domain),
|
||||
]
|
||||
);
|
||||
|
||||
// 2. Add tabs to organize your options.
|
||||
$settingsManager
|
||||
->addTab('general', 'General', 'dashicons-admin-generic')
|
||||
->addTab('advanced', 'Advanced', 'dashicons-admin-settings');
|
||||
->addTab('general', __('General', $text_domain), 'dashicons-admin-generic')
|
||||
->addTab('advanced', __('Advanced', $text_domain), 'dashicons-admin-settings');
|
||||
|
||||
// 3. Add sections to the tabs.
|
||||
$settingsManager
|
||||
->addSection('api_section', 'API Credentials', 'Settings for the external API connection.', 'general')
|
||||
->addSection('display_section', 'Display Options', 'Control the look and feel.', 'general')
|
||||
->addSection('debugging_section', 'Debugging', 'Advanced developer settings.', 'advanced');
|
||||
->addSection('api_section', __('API Credentials', $text_domain), __('Settings for the external API connection.', $text_domain), 'general')
|
||||
->addSection('display_section', __('Display Options', $text_domain), __('Control the look and feel.', $text_domain), 'general')
|
||||
->addSection('debugging_section', __('Debugging', $text_domain), __('Advanced developer settings.', $text_domain), 'advanced');
|
||||
|
||||
// 4. Add fields to the sections.
|
||||
$settingsManager
|
||||
|
@ -96,37 +116,43 @@ add_action('plugins_loaded', function () {
|
|||
'api_key',
|
||||
'api_section',
|
||||
'text',
|
||||
'API Key',
|
||||
['description' => 'Enter your public API key.']
|
||||
__('API Key', $text_domain),
|
||||
['description' => __('Enter your public API key.', $text_domain)]
|
||||
)
|
||||
->addField(
|
||||
'primary_color',
|
||||
'display_section',
|
||||
'color',
|
||||
'Primary Color',
|
||||
['description' => 'Select a primary color for plugin elements.', 'default' => '#2271b1']
|
||||
__('Primary Color', $text_domain),
|
||||
[
|
||||
'description' => __('Select a primary color for plugin elements.', $text_domain),
|
||||
'default' => '#2271b1'
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'brand_logo',
|
||||
'display_section',
|
||||
'media',
|
||||
'Brand Logo',
|
||||
['description' => 'Upload a logo to display in the header.']
|
||||
__('Brand Logo', $text_domain),
|
||||
['description' => __('Upload a logo to display in the header.', $text_domain)]
|
||||
)
|
||||
->addField(
|
||||
'enable_debugging', // This field will control the next one
|
||||
'debugging_section',
|
||||
'toggle',
|
||||
'Enable Debug Mode',
|
||||
['description' => 'When enabled, advanced logging will be active.', 'default' => false]
|
||||
__('Enable Debug Mode', $text_domain),
|
||||
[
|
||||
'description' => __('When enabled, advanced logging will be active.', $text_domain),
|
||||
'default' => false
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'custom_css',
|
||||
'debugging_section',
|
||||
'code', // A code editor field
|
||||
'Custom CSS',
|
||||
__('Custom CSS', $text_domain),
|
||||
[
|
||||
'description' => 'Enter custom CSS to be loaded on the front-end.',
|
||||
'description' => __('Enter custom CSS to be loaded on the front-end.', $text_domain),
|
||||
'language' => 'css', // Specify syntax highlighting mode
|
||||
'conditional' => [
|
||||
'field' => 'enable_debugging', // The ID of the controlling field
|
||||
|
@ -142,12 +168,15 @@ add_action('plugins_loaded', function () {
|
|||
|
||||
|
||||
// You can now use this $settingsManager object to retrieve values.
|
||||
// In a DI container setup, you would bind the SettingsInterface to this instance.
|
||||
// In a DI container setup, you would bind it to the SettingsInterface.
|
||||
// For this example, we'll just show how to use the object directly.
|
||||
|
||||
function my_plugin_get_color()
|
||||
{
|
||||
global $settingsManager; // Example of accessing the object
|
||||
// This is a simplified example. In a real application, avoid using globals.
|
||||
// You would typically pass the $settingsManager object to where it's needed
|
||||
// or retrieve it from a service container.
|
||||
global $settingsManager;
|
||||
return $settingsManager->get('primary_color', '#2271b1');
|
||||
}
|
||||
});
|
||||
|
@ -185,7 +214,7 @@ $settingsManager->addField(
|
|||
'license_key',
|
||||
'general_section',
|
||||
'text',
|
||||
'License Key',
|
||||
__('License Key', 'my-text-domain'),
|
||||
[
|
||||
'conditional' => [
|
||||
'field' => 'license_type', // The ID of the field to check
|
||||
|
@ -198,13 +227,13 @@ $settingsManager->addField(
|
|||
|
||||
### Customizing the HTML Prefix
|
||||
|
||||
By default, all custom CSS classes are prefixed with `wptechnix-settings-` (e.g., `.wptechnix-settings-toggle`). You can provide your own prefix if you want.
|
||||
By default, all custom CSS classes are prefixed with `wptechnix-settings-`. You can provide your own prefix in the
|
||||
constructor's options array.
|
||||
|
||||
```php
|
||||
$settingsManager = new Settings(
|
||||
'my-plugin-slug',
|
||||
'My Plugin',
|
||||
null,
|
||||
$settingsManager = new \WPTechnix\WPSettings\Settings(
|
||||
'my_plugin_options',
|
||||
'my-plugin-settings',
|
||||
[
|
||||
'htmlPrefix' => 'myplugin' // Classes will now be prefixed with `myplugin-`
|
||||
]
|
||||
|
|
|
@ -1,52 +1,56 @@
|
|||
# Command-Line Scripts
|
||||
|
||||
This directory contains all the wrapper scripts used to manage and interact with the project's development environment. These scripts are designed to be run from the project root.
|
||||
This directory contains all the wrapper scripts used to manage and interact with the project's development environment.
|
||||
These scripts are designed to be run from the project root.
|
||||
|
||||
---
|
||||
|
||||
## `bin/docker`
|
||||
|
||||
This is the primary script for interacting with the Docker environment. It's a smart wrapper around `docker compose` that simplifies container management and command execution.
|
||||
This is the primary script for interacting with the Docker environment. It's a smart wrapper around `docker compose`
|
||||
that simplifies container management and command execution.
|
||||
|
||||
### Shorthand Container Access
|
||||
|
||||
The script's default behavior is to execute commands directly inside the main `app` container. Any command that is not a special management command (listed below) is passed through.
|
||||
The script's default behavior is to execute commands directly inside the main `app` container. Any command that is not a
|
||||
special management command (listed below) is passed through.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `./bin/docker` | Open an interactive `bash` shell inside the default `app` container. |
|
||||
| `./bin/docker <cmd...>` | Run any command with its arguments inside the `app` container. |
|
||||
| **Example:** `./bin/docker php -v` | Checks the PHP version inside the container. |
|
||||
| **Example:** `./bin/docker ls -la` | Lists files in the container's default working directory (`/app`). |
|
||||
| Command | Description |
|
||||
|------------------------------------|----------------------------------------------------------------------|
|
||||
| `./bin/docker` | Open an interactive `bash` shell inside the default `app` container. |
|
||||
| `./bin/docker <cmd...>` | Run any command with its arguments inside the `app` container. |
|
||||
| **Example:** `./bin/docker php -v` | Checks the PHP version inside the container. |
|
||||
| **Example:** `./bin/docker ls -la` | Lists files in the container's default working directory (`/app`). |
|
||||
|
||||
### Environment Management Commands
|
||||
|
||||
These special commands are used to control the Docker Compose stack.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `up` | Start all services defined in `docker-compose.yml` in detached mode. |
|
||||
| `down` | Stop and remove all containers, networks, and volumes. |
|
||||
| `build [service...]` | Rebuild and restart services (default: all). |
|
||||
| `restart [service...]` | Restart one or more services (default: all). |
|
||||
| `logs [service...]` | Follow log output from one or more services (default: all). |
|
||||
| `exec <service> <cmd...>` | Execute a command in a **specific** service container. |
|
||||
| **Example:** `exec db mysql` | Opens a MySQL command-line client inside the `db` container. |
|
||||
| Command | Description |
|
||||
|------------------------------|----------------------------------------------------------------------|
|
||||
| `up` | Start all services defined in `docker-compose.yml` in detached mode. |
|
||||
| `down` | Stop and remove all containers, networks, and volumes. |
|
||||
| `build [service...]` | Rebuild and restart services (default: all). |
|
||||
| `restart [service...]` | Restart one or more services (default: all). |
|
||||
| `logs [service...]` | Follow log output from one or more services (default: all). |
|
||||
| `exec <service> <cmd...>` | Execute a command in a **specific** service container. |
|
||||
| **Example:** `exec db mysql` | Opens a MySQL command-line client inside the `db` container. |
|
||||
|
||||
---
|
||||
|
||||
## `bin/composer`
|
||||
|
||||
This is a dedicated wrapper script for Composer. It simplifies running Composer commands by automatically forwarding them to be executed inside the `app` container.
|
||||
This is a dedicated wrapper script for Composer. It simplifies running Composer commands by automatically forwarding
|
||||
them to be executed inside the `app` container.
|
||||
|
||||
Instead of typing `./bin/docker composer <command>`, you can simply use:
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------------------------- | ------------------------------------------------- |
|
||||
| `./bin/composer install` | Install all PHP dependencies from `composer.lock`.|
|
||||
| `./bin/composer update` | Update PHP dependencies to their latest versions. |
|
||||
| `./bin/composer require vendor/package` | Add a new PHP package to the project. |
|
||||
| `./bin/composer remove vendor/package` | Remove a PHP package from the project. |
|
||||
| Command | Description |
|
||||
|-----------------------------------------|----------------------------------------------------|
|
||||
| `./bin/composer install` | Install all PHP dependencies from `composer.lock`. |
|
||||
| `./bin/composer update` | Update PHP dependencies to their latest versions. |
|
||||
| `./bin/composer require vendor/package` | Add a new PHP package to the project. |
|
||||
| `./bin/composer remove vendor/package` | Remove a PHP package from the project. |
|
||||
|
||||
---
|
||||
|
||||
|
@ -58,7 +62,7 @@ These scripts are typically used only during the initial setup of the project.
|
|||
|
||||
This script prepares your local environment by copying all necessary configuration files from their templates.
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------ |
|
||||
| `./bin/copy` | Copies template files (e.g., `.dist`, `.example`) if the destination does not already exist. |
|
||||
| `./bin/copy --override` | Forces the copy, overwriting any existing configuration files. |
|
||||
| Command | Description |
|
||||
|-------------------------|----------------------------------------------------------------------------------------------|
|
||||
| `./bin/copy` | Copies template files (e.g., `.dist`, `.example`) if the destination does not already exist. |
|
||||
| `./bin/copy --override` | Forces the copy, overwriting any existing configuration files. |
|
||||
|
|
|
@ -1,48 +1,48 @@
|
|||
{
|
||||
"name": "wptechnix/wp-settings-framework",
|
||||
"version": "1.0.0",
|
||||
"description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"wordpress",
|
||||
"settings",
|
||||
"options"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "WPTechnix",
|
||||
"email": "developers@wptechnix.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpcompatibility/php-compatibility": "*",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"squizlabs/php_codesniffer": "^3",
|
||||
"szepeviktor/phpstan-wordpress": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WPTechnix\\WPSettings\\": "src/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"fix:phpcbf": "vendor/bin/phpcbf || true",
|
||||
"lint:phpcs": "vendor/bin/phpcs --report=full",
|
||||
"lint:phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --error-format=table",
|
||||
"lint": [
|
||||
"@fix:phpcbf",
|
||||
"@lint:phpcs",
|
||||
"@lint:phpstan"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
"name": "wptechnix/wp-settings-framework",
|
||||
"version": "1.0.0",
|
||||
"description": "A modern, fluent, and object-oriented framework for creating powerful WordPress admin settings pages.",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"wordpress",
|
||||
"settings",
|
||||
"options"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "WPTechnix",
|
||||
"email": "developers@wptechnix.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpcompatibility/php-compatibility": "*",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"squizlabs/php_codesniffer": "^3",
|
||||
"szepeviktor/phpstan-wordpress": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WPTechnix\\WPSettings\\": "src/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"fix:phpcbf": "vendor/bin/phpcbf || true",
|
||||
"lint:phpcs": "vendor/bin/phpcs --report=full",
|
||||
"lint:phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --error-format=table",
|
||||
"lint": [
|
||||
"@fix:phpcbf",
|
||||
"@lint:phpcs",
|
||||
"@lint:phpstan"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
# WPTechnix Settings Framework: Usage & Field Reference
|
||||
|
||||
Welcome to the examples directory for the WPTechnix Settings Framework. This document provides a comprehensive guide to building a settings page and a detailed reference for every available field type and its configuration options.
|
||||
Welcome to the examples directory for the WPTechnix Settings Framework. This document provides a comprehensive guide to
|
||||
building a settings page and a detailed reference for every available field type and its configuration options.
|
||||
|
||||
For installation instructions, please see the main [README.md](../README.md) in the root of the project.
|
||||
|
||||
## 1. Creating a Settings Page
|
||||
|
||||
The foundation of the framework is the `\WPTechnix\WPSettings\Settings` class. You instantiate this class to begin building your page.
|
||||
The foundation of the framework is the `\WPTechnix\WPSettings\Settings` class. You instantiate this class to begin
|
||||
building your page.
|
||||
|
||||
### The `Settings` Class Constructor
|
||||
|
||||
The constructor creates your settings page object.
|
||||
|
||||
```php
|
||||
new Settings(string $pageSlug, ?string $pageTitle = null, ?string $menuTitle = null, array $options = [])```
|
||||
new Settings(string $optionName, string $pageSlug, array $options = [])
|
||||
```
|
||||
|
||||
* `$pageSlug` (string, **required**): A unique slug for your settings page (e.g., `my-plugin-settings`). This is used in the URL.
|
||||
* `$pageTitle` (string|null, optional): The main `<h1>` title displayed at the top of your settings page. If omitted, it defaults to "Settings".
|
||||
* `$menuTitle` (string|null, optional): The text displayed in the WordPress admin menu. If omitted, it defaults to the `$pageTitle`.
|
||||
* `$options` (array, optional): An associative array to override default page settings.
|
||||
* `$optionName` (string, **required**): A unique name for the option where all settings will be stored in the
|
||||
`wp_options` table (e.g., `my_plugin_options`).
|
||||
* `$pageSlug` (string, **required**): A unique slug for your settings page (e.g., `my-plugin-settings`). This is used in
|
||||
the URL.
|
||||
* `$options` (array, optional): An associative array to override default page settings. This is the **recommended** way
|
||||
to set page titles, menu titles, and other configurations, especially for translation.
|
||||
|
||||
#### Constructor Options (`$options` array)
|
||||
|
||||
|
@ -26,9 +31,12 @@ You can pass the following keys in the `$options` array:
|
|||
|
||||
| Key | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `pageTitle` | string | `'Settings'` | The main `<h1>` title displayed at the top of your settings page. |
|
||||
| `menuTitle` | string | `'Settings'` | The text displayed in the WordPress admin menu. |
|
||||
| `parentSlug` | string | `'options-general.php'` | The slug of the parent menu to attach this page to (e.g., `'edit.php?post_type=page'`, `'tools.php'`). |
|
||||
| `capability`| string | `'manage_options'` | The WordPress capability required for a user to view this page. |
|
||||
| `htmlPrefix` | string | `'wptechnix-settings'` | A prefix for all custom HTML classes to prevent CSS/JS conflicts. |
|
||||
| `labels` | array | `[...]` | An array of framework-generated strings you can override for translation. |
|
||||
|
||||
### Basic Structure
|
||||
|
||||
|
@ -39,13 +47,20 @@ Every settings page follows this basic pattern:
|
|||
use WPTechnix\WPSettings\Settings;
|
||||
|
||||
// 1. Instantiate the Settings manager
|
||||
$settingsManager = new Settings('my-plugin-slug', 'My Plugin Settings');
|
||||
$settingsManager = new Settings(
|
||||
'my_plugin_options',
|
||||
'my-plugin-settings',
|
||||
[
|
||||
'pageTitle' => __('My Plugin Settings', 'my-text-domain'),
|
||||
'menuTitle' => __('My Plugin', 'my-text-domain'),
|
||||
]
|
||||
);
|
||||
|
||||
// 2. Add at least one Section
|
||||
$settingsManager->addSection('main_section', 'Main Settings');
|
||||
$settingsManager->addSection('main_section', __('Main Settings', 'my-text-domain'));
|
||||
|
||||
// 3. Add Fields to your section
|
||||
$settingsManager->addField('api_key', 'main_section', 'text', 'API Key');
|
||||
$settingsManager->addField('api_key', 'main_section', 'text', __('API Key', 'my-text-domain'));
|
||||
|
||||
// 4. Initialize the page to hook it into WordPress
|
||||
$settingsManager->init();
|
||||
|
@ -64,118 +79,132 @@ The `addField()` method has the following signature:
|
|||
|
||||
These arguments can be used with almost every field type:
|
||||
|
||||
* `description` (string): Help text displayed below the field. Supports HTML.
|
||||
* `default` (mixed): A default value for the field if none is saved in the database.
|
||||
* `attributes` (array): An associative array of custom HTML attributes to add to the input element (see "Advanced Usage" below).
|
||||
* `conditional` (array): An array to control the field's visibility based on another field's value (see "Advanced Usage" below).
|
||||
* `description` (string): Help text displayed below the field. Supports HTML.
|
||||
* `default` (mixed): A default value for the field if none is saved in the database.
|
||||
* `attributes` (array): An associative array of custom HTML attributes to add to the input element (see "Advanced Usage"
|
||||
below).
|
||||
* `conditional` (array): An array to control the field's visibility based on another field's value (see "Advanced Usage"
|
||||
below).
|
||||
|
||||
---
|
||||
|
||||
### Text & Input Fields
|
||||
|
||||
#### Field: Text (`type: 'text'`)
|
||||
|
||||
A standard single-line text input.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'api_key',
|
||||
'main_section',
|
||||
'text',
|
||||
'API Key',
|
||||
[
|
||||
'description' => 'Enter your public API key.',
|
||||
'attributes' => [
|
||||
'placeholder' => 'pub_xxxxxxxxxx',
|
||||
'class' => 'regular-text code',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'api_key',
|
||||
'main_section',
|
||||
'text',
|
||||
'API Key',
|
||||
[
|
||||
'description' => 'Enter your public API key.',
|
||||
'attributes' => [
|
||||
'placeholder' => 'pub_xxxxxxxxxx',
|
||||
'class' => 'regular-text code',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Email (`type: 'email'`)
|
||||
|
||||
A text input with HTML5 `type="email"` validation.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'admin_email',
|
||||
'main_section',
|
||||
'email',
|
||||
'Admin Email',
|
||||
[
|
||||
'description' => 'The email address for notifications.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'admin_email',
|
||||
'main_section',
|
||||
'email',
|
||||
'Admin Email',
|
||||
[
|
||||
'description' => 'The email address for notifications.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Password (`type: 'password'`)
|
||||
|
||||
A text input where the value is obscured.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'secret_key',
|
||||
'main_section',
|
||||
'password',
|
||||
'Secret Key',
|
||||
[
|
||||
'description' => 'Your secret key will not be shown.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'secret_key',
|
||||
'main_section',
|
||||
'password',
|
||||
'Secret Key',
|
||||
[
|
||||
'description' => 'Your secret key will not be shown.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Number (`type: 'number'`)
|
||||
|
||||
A number input. You can use the `attributes` argument to set `min`, `max`, and `step`.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'item_limit',
|
||||
'main_section',
|
||||
'number',
|
||||
'Item Limit',
|
||||
[
|
||||
'default' => 10,
|
||||
'attributes' => [
|
||||
'min' => 1,
|
||||
'max' => 50,
|
||||
'step' => 1,
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'item_limit',
|
||||
'main_section',
|
||||
'number',
|
||||
'Item Limit',
|
||||
[
|
||||
'default' => 10,
|
||||
'attributes' => [
|
||||
'min' => 1,
|
||||
'max' => 50,
|
||||
'step' => 1,
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: URL (`type: 'url'`)
|
||||
|
||||
A text input with HTML5 `type="url"` validation.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'website_url',
|
||||
'main_section',
|
||||
'url',
|
||||
'Website URL',
|
||||
[
|
||||
'attributes' => [
|
||||
'placeholder' => 'https://example.com',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'website_url',
|
||||
'main_section',
|
||||
'url',
|
||||
'Website URL',
|
||||
[
|
||||
'attributes' => [
|
||||
'placeholder' => 'https://example.com',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Textarea (`type: 'textarea'`)
|
||||
|
||||
A standard multi-line text area. Use `attributes` to control `rows` and `cols`.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'custom_header_text',
|
||||
'main_section',
|
||||
'textarea',
|
||||
'Header Text',
|
||||
[
|
||||
'attributes' => [
|
||||
'rows' => 4,
|
||||
'placeholder' => 'Enter a welcome message...',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'custom_header_text',
|
||||
'main_section',
|
||||
'textarea',
|
||||
'Header Text',
|
||||
[
|
||||
'attributes' => [
|
||||
'rows' => 4,
|
||||
'placeholder' => 'Enter a welcome message...',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
@ -183,228 +212,259 @@ A standard multi-line text area. Use `attributes` to control `rows` and `cols`.
|
|||
|
||||
These fields use a special `options` argument.
|
||||
|
||||
* `options` (array): An associative array where the `key` is the value that gets saved, and the `value` is the display label. `['saved_value' => 'Displayed Label']`
|
||||
* `options` (array): An associative array where the `key` is the value that gets saved, and the `value` is the display
|
||||
label. `['saved_value' => 'Displayed Label']`
|
||||
|
||||
#### Field: Checkbox (`type: 'checkbox'`)
|
||||
|
||||
A single checkbox. Saves `true` if checked, `false` if not.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'enable_tracking',
|
||||
'main_section',
|
||||
'checkbox',
|
||||
'Enable Tracking',
|
||||
[
|
||||
'description' => 'Allow usage data to be collected.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'enable_tracking',
|
||||
'main_section',
|
||||
'checkbox',
|
||||
'Enable Tracking',
|
||||
[
|
||||
'description' => 'Allow usage data to be collected.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Toggle (`type: 'toggle'`)
|
||||
|
||||
A modern on/off toggle switch. Functionally identical to a checkbox.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'dark_mode',
|
||||
'main_section',
|
||||
'toggle',
|
||||
'Enable Dark Mode',
|
||||
[
|
||||
'default' => true,
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'dark_mode',
|
||||
'main_section',
|
||||
'toggle',
|
||||
'Enable Dark Mode',
|
||||
[
|
||||
'default' => true,
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Select (`type: 'select'`)
|
||||
|
||||
A dropdown select menu.
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the dropdown options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'font_size',
|
||||
'main_section',
|
||||
'select',
|
||||
'Font Size',
|
||||
[
|
||||
'default' => 'medium',
|
||||
'options' => [
|
||||
'small' => 'Small',
|
||||
'medium' => 'Medium',
|
||||
'large' => 'Large',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the dropdown options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'font_size',
|
||||
'main_section',
|
||||
'select',
|
||||
'Font Size',
|
||||
[
|
||||
'default' => 'medium',
|
||||
'options' => [
|
||||
'small' => 'Small',
|
||||
'medium' => 'Medium',
|
||||
'large' => 'Large',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Multi-Select (`type: 'multiselect'`)
|
||||
|
||||
A dropdown that allows for multiple selections. Saves an array of values.
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'post_types',
|
||||
'main_section',
|
||||
'multiselect',
|
||||
'Applicable Post Types',
|
||||
[
|
||||
'default' => ['post'],
|
||||
'options' => [
|
||||
'post' => 'Posts',
|
||||
'page' => 'Pages',
|
||||
'product' => 'Products',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'post_types',
|
||||
'main_section',
|
||||
'multiselect',
|
||||
'Applicable Post Types',
|
||||
[
|
||||
'default' => ['post'],
|
||||
'options' => [
|
||||
'post' => 'Posts',
|
||||
'page' => 'Pages',
|
||||
'product' => 'Products',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Radio (`type: 'radio'`)
|
||||
|
||||
A group of radio buttons where only one option can be selected.
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the radio options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'image_alignment',
|
||||
'main_section',
|
||||
'radio',
|
||||
'Image Alignment',
|
||||
[
|
||||
'default' => 'left',
|
||||
'options' => [
|
||||
'left' => 'Align Left',
|
||||
'center' => 'Align Center',
|
||||
'right' => 'Align Right',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the radio options.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'image_alignment',
|
||||
'main_section',
|
||||
'radio',
|
||||
'Image Alignment',
|
||||
[
|
||||
'default' => 'left',
|
||||
'options' => [
|
||||
'left' => 'Align Left',
|
||||
'center' => 'Align Center',
|
||||
'right' => 'Align Right',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Button Group (`type: 'buttongroup'`)
|
||||
|
||||
A modern, styled button group that functions identically to a radio field.
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the buttons.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'layout_style',
|
||||
'main_section',
|
||||
'buttongroup',
|
||||
'Layout Style',
|
||||
[
|
||||
'default' => 'grid',
|
||||
'options' => [
|
||||
'grid' => 'Grid',
|
||||
'list' => 'List',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Configuration Arguments:**
|
||||
* `options` (array, required): The key/value pairs for the buttons.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'layout_style',
|
||||
'main_section',
|
||||
'buttongroup',
|
||||
'Layout Style',
|
||||
[
|
||||
'default' => 'grid',
|
||||
'options' => [
|
||||
'grid' => 'Grid',
|
||||
'list' => 'List',
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Enhanced UI Fields
|
||||
|
||||
#### Field: Color (`type: 'color'`)
|
||||
|
||||
A color picker that uses the native WordPress color picker.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'primary_color',
|
||||
'main_section',
|
||||
'color',
|
||||
'Primary Color',
|
||||
[
|
||||
'default' => '#2271b1',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'primary_color',
|
||||
'main_section',
|
||||
'color',
|
||||
'Primary Color',
|
||||
[
|
||||
'default' => '#2271b1',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Date (`type: 'date'`)
|
||||
|
||||
A date picker input.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('start_date', 'main_section', 'date', 'Campaign Start Date');
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('start_date', 'main_section', 'date', 'Campaign Start Date');
|
||||
```
|
||||
|
||||
#### Field: DateTime (`type: 'datetime'`)
|
||||
|
||||
A date and time picker input.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('event_datetime', 'main_section', 'datetime', 'Event Date & Time');
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('event_datetime', 'main_section', 'datetime', 'Event Date & Time');
|
||||
```
|
||||
|
||||
#### Field: Time (`type: 'time'`)
|
||||
|
||||
A time picker input.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('closing_time', 'main_section', 'time', 'Closing Time');
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('closing_time', 'main_section', 'time', 'Closing Time');
|
||||
```
|
||||
|
||||
#### Field: Range (`type: 'range'`)
|
||||
|
||||
An enhanced slider for selecting a number within a range.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'opacity_level',
|
||||
'main_section',
|
||||
'range',
|
||||
'Opacity Level (%)',
|
||||
[
|
||||
'default' => 80,
|
||||
'attributes' => [
|
||||
'min' => 0,
|
||||
'max' => 100,
|
||||
'step' => 5,
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'opacity_level',
|
||||
'main_section',
|
||||
'range',
|
||||
'Opacity Level (%)',
|
||||
[
|
||||
'default' => 80,
|
||||
'attributes' => [
|
||||
'min' => 0,
|
||||
'max' => 100,
|
||||
'step' => 5,
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Advanced & Special Fields
|
||||
|
||||
#### Field: Media (`type: 'media'`)
|
||||
|
||||
A media uploader that integrates with the WordPress Media Library. It saves the attachment ID.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('site_logo', 'main_section', 'media', 'Site Logo');
|
||||
```
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField('site_logo', 'main_section', 'media', 'Site Logo');
|
||||
```
|
||||
|
||||
#### Field: Code (`type: 'code'`)
|
||||
|
||||
A code editor with syntax highlighting, powered by CodeMirror.
|
||||
* **Configuration Arguments:**
|
||||
* `language` (string): The syntax highlighting mode. Can be `css`, `javascript` (or `js`), or `html`. Defaults to `css`.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'custom_js',
|
||||
'main_section',
|
||||
'code',
|
||||
'Footer JavaScript',
|
||||
[
|
||||
'language' => 'javascript',
|
||||
'description' => 'This code will be added to your site footer.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
* **Configuration Arguments:**
|
||||
* `language` (string): The syntax highlighting mode. Can be `css`, `javascript` (or `js`), or `html`. Defaults to
|
||||
`css`.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'custom_js',
|
||||
'main_section',
|
||||
'code',
|
||||
'Footer JavaScript',
|
||||
[
|
||||
'language' => 'javascript',
|
||||
'description' => 'This code will be added to your site footer.',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
#### Field: Description (`type: 'description'`)
|
||||
A special read-only field used to display information. It has no input and saves no value. The content is passed via the `description` argument.
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'shortcode_info',
|
||||
'main_section',
|
||||
'description',
|
||||
'Shortcode',
|
||||
[
|
||||
'description' => 'To display the form, use the shortcode: <code>[my_awesome_form]</code>',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
A special read-only field used to display information. It has no input and saves no value. The content is passed via the
|
||||
`description` argument.
|
||||
|
||||
* **Example:**
|
||||
```php
|
||||
$settings->addField(
|
||||
'shortcode_info',
|
||||
'main_section',
|
||||
'description',
|
||||
'Shortcode',
|
||||
[
|
||||
'description' => 'To display the form, use the shortcode: <code>[my_awesome_form]</code>',
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
@ -412,9 +472,12 @@ A special read-only field used to display information. It has no input and saves
|
|||
|
||||
### Custom HTML Attributes (`attributes`)
|
||||
|
||||
The `attributes` argument gives you direct access to the HTML input element. You can pass an associative array of any valid HTML attribute, and it will be added to the field. This is incredibly powerful for adding placeholders, data attributes, or accessibility enhancements.
|
||||
The `attributes` argument gives you direct access to the HTML input element. You can pass an associative array of any
|
||||
valid HTML attribute, and it will be added to the field. This is incredibly powerful for adding placeholders, data
|
||||
attributes, or accessibility enhancements.
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
$settings->addField(
|
||||
'api_key',
|
||||
|
@ -434,13 +497,15 @@ $settings->addField(
|
|||
|
||||
### Conditional Logic (`conditional`)
|
||||
|
||||
The `conditional` argument makes a field appear only when another field meets a certain condition. It's an array with three keys:
|
||||
The `conditional` argument makes a field appear only when another field meets a certain condition. It's an array with
|
||||
three keys:
|
||||
|
||||
* `field` (string, required): The ID of the field to watch.
|
||||
* `value` (string, required): The value the watched field must have. For toggles/checkboxes, use `'1'` for "on".
|
||||
* `operator` (string, optional): The comparison operator. Can be `==` (default), `!=`, `in`, or `not in`.
|
||||
* `field` (string, required): The ID of the field to watch.
|
||||
* `value` (string, required): The value the watched field must have. For toggles/checkboxes, use `'1'` for "on".
|
||||
* `operator` (string, optional): The comparison operator. Can be `==` (default), `!=`, `in`, or `not in`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```php
|
||||
// The controlling field
|
||||
$settings->addField(
|
||||
|
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
use WPTechnix\WPSettings\Settings;
|
||||
|
||||
// Please make sure you require the composer autoload.
|
||||
// require_once plugin_dir_path(__FILE__) . '/vendor/autoload.php';
|
||||
|
||||
add_action('plugins_loaded', 'wptechnix_settings_demo_with_tabs');
|
||||
|
||||
/**
|
||||
|
@ -13,10 +16,13 @@ function wptechnix_settings_demo_with_tabs(): void
|
|||
{
|
||||
// 1. Create a new Settings instance.
|
||||
$settings = new Settings(
|
||||
'wptechnix-demo-tabs', // Unique Page Slug
|
||||
'Settings Demo (Tabs)', // Page Title
|
||||
'Settings Demo (Tabs)', // Menu Title
|
||||
['parentSlug' => 'tools.php'] // Place this page under the "Tools" menu.
|
||||
'wptechnix_options_tabs', // Unique option name for the database
|
||||
'wptechnix-demo-tabs', // Unique page slug
|
||||
[
|
||||
'pageTitle' => 'Settings Demo (Tabs)', // Page Title
|
||||
'menuTitle' => 'Settings Demo (Tabs)', // Menu Title
|
||||
'parentSlug' => 'tools.php' // Place this page under the "Tools" menu.
|
||||
]
|
||||
);
|
||||
|
||||
// 2. Add tabs to organize the settings page.
|
||||
|
@ -26,10 +32,25 @@ function wptechnix_settings_demo_with_tabs(): void
|
|||
|
||||
// 3. Add sections and assign them to the correct tabs.
|
||||
$settings->addSection('text_inputs', 'Text-Based Inputs', 'Fields for text, numbers, and passwords.', 'inputs')
|
||||
->addSection('choice_inputs', 'Choice-Based Inputs', 'Fields for selecting one or more options.', 'choices')
|
||||
->addSection(
|
||||
'choice_inputs',
|
||||
'Choice-Based Inputs',
|
||||
'Fields for selecting one or more options.',
|
||||
'choices'
|
||||
)
|
||||
->addSection('ui_inputs', 'Enhanced UI Inputs', 'Fields with special user interfaces.', 'choices')
|
||||
->addSection('advanced_inputs', 'Advanced & Special Inputs', 'Media, code, and other powerful fields.', 'advanced')
|
||||
->addSection('conditional_section', 'Conditional Logic Demo', 'Show and hide fields based on other fields\' values.', 'advanced');
|
||||
->addSection(
|
||||
'advanced_inputs',
|
||||
'Advanced & Special Inputs',
|
||||
'Media, code, and other powerful fields.',
|
||||
'advanced'
|
||||
)
|
||||
->addSection(
|
||||
'conditional_section',
|
||||
'Conditional Logic Demo',
|
||||
'Show and hide fields based on other fields\' values.',
|
||||
'advanced'
|
||||
);
|
||||
|
||||
// --- FIELDS FOR "INPUTS & TEXT" TAB ---
|
||||
$settings
|
||||
|
@ -44,10 +65,34 @@ function wptechnix_settings_demo_with_tabs(): void
|
|||
$settings
|
||||
->addField('demo_checkbox', 'choice_inputs', 'checkbox', 'Checkbox Field')
|
||||
->addField('demo_toggle', 'choice_inputs', 'toggle', 'Toggle Switch', ['default' => true])
|
||||
->addField('demo_select', 'choice_inputs', 'select', 'Select Dropdown', ['options' => ['a' => 'Option A', 'b' => 'Option B']])
|
||||
->addField('demo_multiselect', 'choice_inputs', 'multiselect', 'Multi-Select', ['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C']])
|
||||
->addField('demo_radio', 'choice_inputs', 'radio', 'Radio Buttons', ['options' => ['yes' => 'Yes', 'no' => 'No']])
|
||||
->addField('demo_buttongroup', 'choice_inputs', 'buttongroup', 'Button Group', ['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']])
|
||||
->addField(
|
||||
'demo_select',
|
||||
'choice_inputs',
|
||||
'select',
|
||||
'Select Dropdown',
|
||||
['options' => ['a' => 'Option A', 'b' => 'Option B'], 'placeholder' => 'Select Option']
|
||||
)
|
||||
->addField(
|
||||
'demo_multiselect',
|
||||
'choice_inputs',
|
||||
'multiselect',
|
||||
'Multi-Select',
|
||||
['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C'], 'placeholder' => 'Select Options']
|
||||
)
|
||||
->addField(
|
||||
'demo_radio',
|
||||
'choice_inputs',
|
||||
'radio',
|
||||
'Radio Buttons',
|
||||
['options' => ['yes' => 'Yes', 'no' => 'No']]
|
||||
)
|
||||
->addField(
|
||||
'demo_buttongroup',
|
||||
'choice_inputs',
|
||||
'buttongroup',
|
||||
'Button Group',
|
||||
['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']]
|
||||
)
|
||||
->addField('demo_color', 'ui_inputs', 'color', 'Color Picker')
|
||||
->addField('demo_date', 'ui_inputs', 'date', 'Date Picker')
|
||||
->addField('demo_datetime', 'ui_inputs', 'datetime', 'Date & Time Picker')
|
||||
|
@ -58,31 +103,40 @@ function wptechnix_settings_demo_with_tabs(): void
|
|||
$settings
|
||||
->addField('demo_media', 'advanced_inputs', 'media', 'Media Uploader')
|
||||
->addField(
|
||||
'demo_code_html', 'advanced_inputs', 'code', 'Code Editor (HTML)',
|
||||
'demo_code_html',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (HTML)',
|
||||
[
|
||||
'description' => 'A code editor with HTML syntax highlighting.',
|
||||
'language' => 'html',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
'language' => 'html',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_code_css', 'advanced_inputs', 'code', 'Code Editor (CSS)',
|
||||
'demo_code_css',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (CSS)',
|
||||
[
|
||||
'description' => 'A code editor with CSS syntax highlighting.',
|
||||
'language' => 'css',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
'language' => 'css',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_code_js', 'advanced_inputs', 'code', 'Code Editor (JS)',
|
||||
'demo_code_js',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (JS)',
|
||||
[
|
||||
'description' => 'A code editor with JavaScript syntax highlighting.',
|
||||
'language' => 'javascript',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
'language' => 'javascript',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_description', 'advanced_inputs', 'description', 'Description Field',
|
||||
'demo_description',
|
||||
'advanced_inputs',
|
||||
'description',
|
||||
'Description Field',
|
||||
['description' => 'This is a read-only field used to display important information. It supports <strong>HTML</strong>.']
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
use WPTechnix\WPSettings\Settings;
|
||||
|
||||
// Please make sure you require the composer autoload.
|
||||
// require_once plugin_dir_path(__FILE__) . '/vendor/autoload.php';
|
||||
|
||||
add_action('plugins_loaded', 'wptechnix_settings_demo_without_tabs');
|
||||
|
||||
/**
|
||||
|
@ -13,123 +16,166 @@ function wptechnix_settings_demo_without_tabs(): void
|
|||
{
|
||||
// 1. Create a new Settings instance.
|
||||
$settings = new Settings(
|
||||
'wptechnix-demo-simple', // Unique Page Slug
|
||||
'Settings Demo (Simple)', // Page Title
|
||||
'Settings Demo (Simple)' // Menu Title
|
||||
'wptechnix_options_no_tabs', // Unique option name for the database
|
||||
'wptechnix-demo-no-tabs', // Unique page slug
|
||||
[
|
||||
'pageTitle' => 'Settings Demo (No Tabs)', // Page Title
|
||||
'menuTitle' => 'Settings Demo (No Tabs)', // Menu Title
|
||||
'parentSlug' => 'tools.php' // Place this page under the "Tools" menu.
|
||||
]
|
||||
);
|
||||
|
||||
// 2. Add sections to organize fields.
|
||||
// 2. Add sections directly to the page (no tabs are needed).
|
||||
$settings->addSection('text_inputs', 'Text-Based Inputs', 'Fields for text, numbers, and passwords.')
|
||||
->addSection(
|
||||
'choice_inputs',
|
||||
'Choice-Based Inputs',
|
||||
'Fields for selecting one or more options.'
|
||||
)
|
||||
->addSection('ui_inputs', 'Enhanced UI Inputs', 'Fields with special user interfaces.')
|
||||
->addSection(
|
||||
'advanced_inputs',
|
||||
'Advanced & Special Inputs',
|
||||
'Media, code, and other powerful fields.'
|
||||
)
|
||||
->addSection(
|
||||
'conditional_section',
|
||||
'Conditional Logic Demo',
|
||||
'Show and hide fields based on other fields\' values.'
|
||||
);
|
||||
|
||||
// 3. Add fields and assign them to the correct sections.
|
||||
|
||||
// --- FIELDS FOR "Text-Based Inputs" SECTION ---
|
||||
$settings
|
||||
->addSection(
|
||||
'basic_fields_section',
|
||||
'Basic Input Fields',
|
||||
'A showcase of standard text, choice, and UI fields.'
|
||||
->addField('demo_text', 'text_inputs', 'text', 'Text Field')
|
||||
->addField('demo_email', 'text_inputs', 'email', 'Email Field')
|
||||
->addField('demo_password', 'text_inputs', 'password', 'Password Field')
|
||||
->addField('demo_number', 'text_inputs', 'number', 'Number Field', ['default' => 42])
|
||||
->addField('demo_url', 'text_inputs', 'url', 'URL Field')
|
||||
->addField('demo_textarea', 'text_inputs', 'textarea', 'Textarea Field');
|
||||
|
||||
// --- FIELDS FOR "Choice-Based Inputs" SECTION ---
|
||||
$settings
|
||||
->addField('demo_checkbox', 'choice_inputs', 'checkbox', 'Checkbox Field')
|
||||
->addField('demo_toggle', 'choice_inputs', 'toggle', 'Toggle Switch', ['default' => true])
|
||||
->addField(
|
||||
'demo_select',
|
||||
'choice_inputs',
|
||||
'select',
|
||||
'Select Dropdown',
|
||||
['options' => ['a' => 'Option A', 'b' => 'Option B'], 'placeholder' => 'Select Option']
|
||||
)
|
||||
->addSection(
|
||||
'advanced_fields_section',
|
||||
'Advanced & Conditional Fields',
|
||||
'A showcase of advanced fields and conditional logic.'
|
||||
->addField(
|
||||
'demo_multiselect',
|
||||
'choice_inputs',
|
||||
'multiselect',
|
||||
'Multi-Select',
|
||||
['options' => ['a' => 'Choice A', 'b' => 'Choice B', 'c' => 'Choice C'], 'placeholder' => 'Select Options']
|
||||
)
|
||||
->addField(
|
||||
'demo_radio',
|
||||
'choice_inputs',
|
||||
'radio',
|
||||
'Radio Buttons',
|
||||
['options' => ['yes' => 'Yes', 'no' => 'No']]
|
||||
)
|
||||
->addField(
|
||||
'demo_buttongroup',
|
||||
'choice_inputs',
|
||||
'buttongroup',
|
||||
'Button Group',
|
||||
['options' => ['left' => 'Left', 'center' => 'Center', 'right' => 'Right']]
|
||||
);
|
||||
|
||||
|
||||
// 3. Add all field types to the sections.
|
||||
// --- FIELDS FOR "Enhanced UI Inputs" SECTION ---
|
||||
$settings
|
||||
// --- Fields for the "Basic Inputs" Section ---
|
||||
->addField(
|
||||
'demo_text', 'basic_fields_section', 'text', 'Text Field',
|
||||
['description' => 'A standard single-line text input.', 'attributes' => ['placeholder' => 'Enter some text...']]
|
||||
)
|
||||
->addField(
|
||||
'demo_textarea', 'basic_fields_section', 'textarea', 'Textarea Field',
|
||||
['description' => 'A multi-line text input area.', 'attributes' => ['rows' => 4]]
|
||||
)
|
||||
->addField(
|
||||
'demo_toggle', 'basic_fields_section', 'toggle', 'Toggle Switch',
|
||||
['description' => 'A modern on/off toggle switch.', 'default' => true]
|
||||
)
|
||||
->addField(
|
||||
'demo_select', 'basic_fields_section', 'select', 'Select Dropdown',
|
||||
['options' => ['option_1' => 'Option One', 'option_2' => 'Option Two', 'option_3' => 'Option Three']]
|
||||
)
|
||||
->addField(
|
||||
'demo_radio', 'basic_fields_section', 'radio', 'Radio Buttons',
|
||||
['options' => ['yes' => 'Yes', 'no' => 'No', 'maybe' => 'Maybe'], 'default' => 'yes']
|
||||
)
|
||||
->addField(
|
||||
'demo_color', 'basic_fields_section', 'color', 'Color Picker',
|
||||
['description' => 'A field for selecting a hex color value.', 'default' => '#52ACCC']
|
||||
)
|
||||
->addField(
|
||||
'demo_date', 'basic_fields_section', 'date', 'Date Picker',
|
||||
['description' => 'A field for selecting a calendar date.']
|
||||
)
|
||||
->addField('demo_color', 'ui_inputs', 'color', 'Color Picker')
|
||||
->addField('demo_date', 'ui_inputs', 'date', 'Date Picker')
|
||||
->addField('demo_datetime', 'ui_inputs', 'datetime', 'Date & Time Picker')
|
||||
->addField('demo_time', 'ui_inputs', 'time', 'Time Picker')
|
||||
->addField('demo_range', 'ui_inputs', 'range', 'Range Slider', ['default' => 75]);
|
||||
|
||||
// --- Fields for the "Advanced & Conditional" Section ---
|
||||
// --- FIELDS FOR "Advanced & Special Inputs" SECTION ---
|
||||
$settings
|
||||
->addField('demo_media', 'advanced_inputs', 'media', 'Media Uploader')
|
||||
->addField(
|
||||
'demo_media', 'advanced_fields_section', 'media', 'Media Uploader',
|
||||
['description' => 'Upload an image or file using the WordPress Media Library.']
|
||||
)
|
||||
->addField(
|
||||
'demo_code_html', 'advanced_fields_section', 'code', 'Code Editor (HTML)',
|
||||
'demo_code_html',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (HTML)',
|
||||
[
|
||||
'description' => 'A code editor with HTML syntax highlighting.',
|
||||
'language' => 'html',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
'language' => 'html',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_code_css', 'advanced_fields_section', 'code', 'Code Editor (CSS)',
|
||||
'demo_code_css',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (CSS)',
|
||||
[
|
||||
'description' => 'A code editor with CSS syntax highlighting.',
|
||||
'language' => 'css',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
'language' => 'css',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_code_js', 'advanced_fields_section', 'code', 'Code Editor (JS)',
|
||||
'demo_code_js',
|
||||
'advanced_inputs',
|
||||
'code',
|
||||
'Code Editor (JS)',
|
||||
[
|
||||
'description' => 'A code editor with JavaScript syntax highlighting.',
|
||||
'language' => 'javascript',
|
||||
'attributes' => ['placeholder' => ''],
|
||||
]
|
||||
)
|
||||
->addField( // --- Start Conditional Logic Demo ---
|
||||
'demo_enable_advanced', // This is the CONTROLLING field
|
||||
'advanced_fields_section',
|
||||
'toggle',
|
||||
'Enable Advanced Options',
|
||||
['description' => 'Turn this on to reveal hidden advanced fields below.', 'default' => false]
|
||||
)
|
||||
->addField(
|
||||
'demo_conditional_api_key', // This is the CONDITIONAL field
|
||||
'advanced_fields_section',
|
||||
'text',
|
||||
'Conditional API Key',
|
||||
[
|
||||
'description' => 'This field is only visible when the toggle above is ON.',
|
||||
'conditional' => [
|
||||
'field' => 'demo_enable_advanced', // The ID of the controlling field
|
||||
'value' => '1', // The value to check for (1 for 'on')
|
||||
'operator' => '==', // The comparison operator
|
||||
]
|
||||
'language' => 'javascript',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_conditional_mode', // This is another CONDITIONAL field
|
||||
'advanced_fields_section',
|
||||
'buttongroup',
|
||||
'Conditional Mode',
|
||||
[
|
||||
'description' => 'This button group is also only visible when the toggle is ON.',
|
||||
'options' => ['live' => 'Live', 'test' => 'Test'],
|
||||
'default' => 'test',
|
||||
'conditional' => [
|
||||
'field' => 'demo_enable_advanced',
|
||||
'value' => '1',
|
||||
]
|
||||
]
|
||||
); // --- End Conditional Logic Demo ---
|
||||
'demo_description',
|
||||
'advanced_inputs',
|
||||
'description',
|
||||
'Description Field',
|
||||
['description' => 'This is a read-only field used to display important information. It supports <strong>HTML</strong>.']
|
||||
);
|
||||
|
||||
// --- FIELDS FOR "Conditional Logic Demo" SECTION ---
|
||||
$settings
|
||||
->addField(
|
||||
'demo_contact_method', // The CONTROLLING field
|
||||
'conditional_section',
|
||||
'radio',
|
||||
'Preferred Contact Method',
|
||||
[
|
||||
'description' => 'Select a method to see different conditional fields appear.',
|
||||
'options' => ['email' => 'Email', 'phone' => 'Phone Call', 'none' => 'No Contact'],
|
||||
'default' => 'none',
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_conditional_email', // A CONDITIONAL field
|
||||
'conditional_section',
|
||||
'email',
|
||||
'Contact Email Address',
|
||||
[
|
||||
'description' => 'This only appears if "Email" is selected.',
|
||||
'conditional' => [
|
||||
'field' => 'demo_contact_method',
|
||||
'value' => 'email',
|
||||
],
|
||||
]
|
||||
)
|
||||
->addField(
|
||||
'demo_conditional_phone', // Another CONDITIONAL field
|
||||
'conditional_section',
|
||||
'text',
|
||||
'Contact Phone Number',
|
||||
[
|
||||
'description' => 'This only appears if "Phone Call" is selected.',
|
||||
'conditional' => [
|
||||
'field' => 'demo_contact_method',
|
||||
'value' => 'phone',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// 4. Initialize the settings page.
|
||||
$settings->init();
|
||||
|
|
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -2923,15 +2923,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
|
@ -3163,13 +3159,6 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
|
@ -4396,13 +4385,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz",
|
||||
"integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==",
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -4481,13 +4470,6 @@
|
|||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
|
|
|
@ -4,28 +4,24 @@ declare(strict_types=1);
|
|||
|
||||
namespace WPTechnix\WPSettings;
|
||||
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
|
||||
/**
|
||||
* Handles the enqueueing and rendering of all static assets.
|
||||
* Handles the enqueueing and rendering of all static assets for the settings page.
|
||||
*
|
||||
* This class centralizes asset management. It intelligently loads external
|
||||
* libraries and uses a configurable HTML class prefix to prevent conflicts.
|
||||
* @noinspection JSUnusedLocalSymbols, CssUnusedSymbol
|
||||
*
|
||||
* @phpstan-import-type SettingsConfig from Settings
|
||||
*/
|
||||
final class AssetManager
|
||||
{
|
||||
/**
|
||||
* The complete settings configuration array.
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config = [];
|
||||
|
||||
/**
|
||||
* Defines the external libraries that can be loaded as fallbacks.
|
||||
* Defines the external libraries that can be loaded.
|
||||
*
|
||||
* @var array<string, array{
|
||||
* handle: string,
|
||||
* script?: array<string, mixed>,
|
||||
* style?: array<string, mixed>
|
||||
* script?: array{src: string, deps: string[], version: string, in_footer: bool},
|
||||
* style?: array{src: string, deps: string[], version: string}
|
||||
* }>
|
||||
*/
|
||||
private array $libraryPackages;
|
||||
|
@ -39,55 +35,58 @@ final class AssetManager
|
|||
|
||||
/**
|
||||
* AssetManager constructor.
|
||||
*
|
||||
* @param ConfigInterface $config Settings Config.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->libraryPackages = [
|
||||
'select2' => [
|
||||
'handle' => 'select2',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js',
|
||||
'deps' => ['jquery'], 'version' => '4.0.13', 'in_footer' => true,
|
||||
public function __construct(
|
||||
protected ConfigInterface $config
|
||||
) {
|
||||
$this->config->deepMerge([
|
||||
'assetPackages' => [
|
||||
'select2' => [
|
||||
'handle' => 'select2',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js',
|
||||
'deps' => ['jquery'],
|
||||
'version' => '4.0.13',
|
||||
'in_footer' => true,
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css',
|
||||
'deps' => [],
|
||||
'version' => '4.0.13',
|
||||
],
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css',
|
||||
'deps' => [], 'version' => '4.0.13',
|
||||
'flatpickr' => [
|
||||
'handle' => 'flatpickr',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js',
|
||||
'deps' => [],
|
||||
'version' => '4.6.13',
|
||||
'in_footer' => true,
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css',
|
||||
'deps' => [],
|
||||
'version' => '4.6.13',
|
||||
],
|
||||
],
|
||||
],
|
||||
'flatpickr' => [
|
||||
'handle' => 'flatpickr',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js',
|
||||
'deps' => [], 'version' => '4.6.13', 'in_footer' => true,
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css',
|
||||
'deps' => [], 'version' => '4.6.13',
|
||||
],
|
||||
],
|
||||
];
|
||||
]
|
||||
]);
|
||||
|
||||
$this->libraryPackages = $this->config->get('assetPackages');
|
||||
|
||||
$this->fieldTypeToPackageMap = [
|
||||
'select' => 'select2',
|
||||
'select' => 'select2',
|
||||
'multiselect' => 'select2',
|
||||
'date' => 'flatpickr',
|
||||
'datetime' => 'flatpickr',
|
||||
'time' => 'flatpickr',
|
||||
'date' => 'flatpickr',
|
||||
'datetime' => 'flatpickr',
|
||||
'time' => 'flatpickr',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the settings configuration array.
|
||||
*
|
||||
* @param array<string, mixed> $config The settings configuration.
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks the enqueue method into WordPress.
|
||||
* Hooks the asset enqueueing method into WordPress.
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
|
@ -95,13 +94,16 @@ final class AssetManager
|
|||
}
|
||||
|
||||
/**
|
||||
* Enqueues all necessary scripts and styles.
|
||||
* Enqueues all necessary scripts and styles for the settings page.
|
||||
*
|
||||
* This method is hooked into 'admin_enqueue_scripts' and only loads assets
|
||||
* on the relevant admin page.
|
||||
*/
|
||||
public function enqueueAssets(): void
|
||||
{
|
||||
$pageSlug = $this->config['pageSlug'] ?? '';
|
||||
$screen = get_current_screen();
|
||||
if (empty($pageSlug) || null === $screen || !str_contains($screen->id, $pageSlug)) {
|
||||
$pageSlug = $this->config->get('pageSlug');
|
||||
$screen = get_current_screen();
|
||||
if (empty($pageSlug) || null === $screen || ! str_contains($screen->id, $pageSlug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -113,7 +115,7 @@ final class AssetManager
|
|||
$this->enqueueRequiredLibraries();
|
||||
$this->enqueueCodeEditorAssets();
|
||||
|
||||
wp_add_inline_script('jquery', $this->getInlineScripts());
|
||||
wp_add_inline_script('jquery-core', $this->getInlineScripts());
|
||||
wp_add_inline_style('wp-admin', $this->getInlineStyles());
|
||||
}
|
||||
|
||||
|
@ -122,15 +124,18 @@ final class AssetManager
|
|||
*/
|
||||
private function enqueueRequiredLibraries(): void
|
||||
{
|
||||
if (empty($this->config['fields'])) {
|
||||
if (! $this->config->has('fields')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = $this->config->get('fields');
|
||||
|
||||
$requiredPackages = [];
|
||||
foreach ($this->config['fields'] as $field) {
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$fieldType = $field['type'] ?? '';
|
||||
if (isset($this->fieldTypeToPackageMap[$fieldType])) {
|
||||
$packageName = $this->fieldTypeToPackageMap[$fieldType];
|
||||
$packageName = $this->fieldTypeToPackageMap[$fieldType];
|
||||
$requiredPackages[$packageName] = true;
|
||||
}
|
||||
}
|
||||
|
@ -143,52 +148,48 @@ final class AssetManager
|
|||
}
|
||||
|
||||
/**
|
||||
* Enqueues a library package, checking for existing registrations first.
|
||||
* Enqueues a single library package (style and/or script).
|
||||
*
|
||||
* It checks if the asset is already registered to avoid conflicts with other plugins.
|
||||
*
|
||||
* @param array{
|
||||
* handle: string,
|
||||
* script?: array<string, mixed>,
|
||||
* style?: array<string, mixed>
|
||||
* script?: array{src: string, deps: string[], version: string, in_footer: bool},
|
||||
* style?: array{src: string, deps: string[], version: string}
|
||||
* } $package The package definition.
|
||||
*/
|
||||
private function enqueuePackage(array $package): void
|
||||
{
|
||||
$handle = $package['handle'];
|
||||
if (isset($package['style'])) {
|
||||
if (!wp_style_is($handle, 'registered')) {
|
||||
wp_register_style(
|
||||
$handle,
|
||||
$package['style']['src'],
|
||||
$package['style']['deps'],
|
||||
$package['style']['version']
|
||||
);
|
||||
if (! wp_style_is($handle, 'registered')) {
|
||||
wp_register_style($handle, ...array_values($package['style']));
|
||||
}
|
||||
wp_enqueue_style($handle);
|
||||
}
|
||||
if (isset($package['script'])) {
|
||||
if (!wp_script_is($handle, 'registered')) {
|
||||
wp_register_script(
|
||||
$handle,
|
||||
$package['script']['src'],
|
||||
$package['script']['deps'],
|
||||
$package['script']['version'],
|
||||
$package['script']['in_footer']
|
||||
);
|
||||
if (! wp_script_is($handle, 'registered')) {
|
||||
wp_register_script($handle, ...array_values($package['script']));
|
||||
}
|
||||
wp_enqueue_script($handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects required CodeMirror languages and enqueues them.
|
||||
* Enqueues the WordPress Code Editor assets for the required languages.
|
||||
*/
|
||||
private function enqueueCodeEditorAssets(): void
|
||||
{
|
||||
if (!function_exists('wp_enqueue_code_editor') || empty($this->config['fields'])) {
|
||||
if (! function_exists('wp_enqueue_code_editor') || ! $this->config->has('fields')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = $this->config->get('fields');
|
||||
|
||||
$requiredLanguages = [];
|
||||
foreach ($this->config['fields'] as $field) {
|
||||
if (($field['type'] ?? '') === 'code') {
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if ('code' === ($field['type'] ?? '')) {
|
||||
$requiredLanguages[$field['language'] ?? 'css'] = true;
|
||||
}
|
||||
}
|
||||
|
@ -196,16 +197,14 @@ final class AssetManager
|
|||
return;
|
||||
}
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'text/javascript',
|
||||
'css' => 'text/css',
|
||||
'js' => 'text/javascript',
|
||||
'javascript' => 'text/javascript',
|
||||
'html' => 'text/html',
|
||||
'xml' => 'application/xml',
|
||||
'html' => 'text/html',
|
||||
'xml' => 'application/xml',
|
||||
];
|
||||
foreach (array_keys($requiredLanguages) as $lang) {
|
||||
wp_enqueue_code_editor([
|
||||
'type' => $mimeTypes[$lang] ?? 'text/plain'
|
||||
]);
|
||||
wp_enqueue_code_editor(['type' => $mimeTypes[$lang] ?? 'text/plain']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,27 +215,24 @@ final class AssetManager
|
|||
*/
|
||||
private function getInlineScripts(): string
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
|
||||
$addMediaTitle = esc_js($this->config->get('labels.addMediaTitle', 'Add media'));
|
||||
$selectMediaText = esc_js($this->config->get('labels.selectMediaText', 'Select'));
|
||||
$removeMediaText = esc_js($this->config->get('labels.removeMediaText', 'Remove'));
|
||||
|
||||
// phpcs:disable Generic.Files.LineLength
|
||||
return <<<JS
|
||||
jQuery(function($) {
|
||||
// Initialize WordPress color picker
|
||||
if ($.fn.wpColorPicker) {
|
||||
$('.{$htmlPrefix}-color-picker').wpColorPicker({
|
||||
change: function(event, ui) {
|
||||
$(event.target).trigger('change');
|
||||
}
|
||||
});
|
||||
$('.{$htmlPrefix}-color-picker').wpColorPicker();
|
||||
}
|
||||
|
||||
// Initialize Select2
|
||||
if ($.fn.select2) {
|
||||
try {
|
||||
$('.{$htmlPrefix}-select2-field').not('.select2-hidden-accessible').select2({
|
||||
width: '100%',
|
||||
allowClear: true
|
||||
});
|
||||
$('.{$htmlPrefix}-select2-field').not('.select2-hidden-accessible').select2({ width: '100%', allowClear: true });
|
||||
} catch (e) {
|
||||
console.error('Settings Framework: Select2 Error:', e);
|
||||
}
|
||||
|
@ -244,23 +240,9 @@ final class AssetManager
|
|||
|
||||
// Initialize Flatpickr
|
||||
if (typeof flatpickr !== 'undefined') {
|
||||
$('.{$htmlPrefix}-flatpickr-date').flatpickr({
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'F j, Y'
|
||||
});
|
||||
$('.{$htmlPrefix}-flatpickr-datetime').flatpickr({
|
||||
enableTime: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
altInput: true,
|
||||
altFormat: 'F j, Y at h:i K'
|
||||
});
|
||||
$('.{$htmlPrefix}-flatpickr-time').flatpickr({
|
||||
enableTime: true,
|
||||
noCalendar: true,
|
||||
dateFormat: 'H:i',
|
||||
time_24hr: true
|
||||
});
|
||||
$('.{$htmlPrefix}-flatpickr-date').flatpickr({ dateFormat: 'Y-m-d', altInput: true, altFormat: 'F j, Y' });
|
||||
$('.{$htmlPrefix}-flatpickr-datetime').flatpickr({ enableTime: true, dateFormat: 'Y-m-d H:i', altInput: true, altFormat: 'F j, Y at h:i K' });
|
||||
$('.{$htmlPrefix}-flatpickr-time').flatpickr({ enableTime: true, noCalendar: true, dateFormat: 'H:i', time_24hr: true });
|
||||
}
|
||||
|
||||
// Initialize WordPress media uploader
|
||||
|
@ -269,27 +251,23 @@ final class AssetManager
|
|||
var button = $(this);
|
||||
var fieldId = button.data('field');
|
||||
var mediaUploader = wp.media({
|
||||
title: 'Select Media',
|
||||
button: {
|
||||
text: 'Use this media'
|
||||
},
|
||||
title: '{$addMediaTitle}',
|
||||
button: { text: '{$selectMediaText}' },
|
||||
multiple: false
|
||||
});
|
||||
mediaUploader.on('select', function() {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
$('#' + fieldId).val(attachment.id).trigger('change');
|
||||
var preview = button.siblings('.{$htmlPrefix}-media-preview');
|
||||
var previewContainer = button.siblings('.{$htmlPrefix}-media-preview');
|
||||
var previewHTML = '';
|
||||
if (attachment.type === 'image') {
|
||||
preview.html(
|
||||
'<img src="' + attachment.url + '" alt="" style="max-width: 150px; height: auto; margin-top: 10px;" />'
|
||||
);
|
||||
previewHTML = '<img src="' + attachment.url + '" alt="" class="{$htmlPrefix}-media-preview-image" />';
|
||||
} else {
|
||||
preview.html(
|
||||
'<p style="margin-top:10px;"><strong>File:</strong> ' + attachment.filename + '</p>'
|
||||
);
|
||||
previewHTML = '<div class="{$htmlPrefix}-media-preview-file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</div>';
|
||||
}
|
||||
previewContainer.html(previewHTML);
|
||||
if (!button.siblings('.{$htmlPrefix}-media-remove-button').length) {
|
||||
button.after(' <button type="button" class="button {$htmlPrefix}-media-remove-button" data-field="' + fieldId + '">Remove</button>');
|
||||
button.after(' <button type="button" class="button {$htmlPrefix}-media-remove-button" data-field="' + fieldId + '">{$removeMediaText}</button>');
|
||||
}
|
||||
});
|
||||
mediaUploader.open();
|
||||
|
@ -303,7 +281,7 @@ final class AssetManager
|
|||
button.remove();
|
||||
});
|
||||
|
||||
// Initialize button groups.
|
||||
// Initialize button groups
|
||||
$('.{$htmlPrefix}-buttongroup-container').on('click', '.{$htmlPrefix}-buttongroup-option', function(e) {
|
||||
e.preventDefault();
|
||||
var button = $(this);
|
||||
|
@ -317,42 +295,31 @@ final class AssetManager
|
|||
// Range sliders
|
||||
function updateRangeSlider(slider) {
|
||||
var value = slider.val();
|
||||
slider
|
||||
.closest('.{$htmlPrefix}-enhanced-range-container')
|
||||
.find('.{$htmlPrefix}-range-value-input').val(value);
|
||||
slider.closest('.{$htmlPrefix}-enhanced-range-container').find('.{$htmlPrefix}-range-value-input').val(value);
|
||||
}
|
||||
$('.{$htmlPrefix}-enhanced-range-slider').on('input', function() {
|
||||
updateRangeSlider($(this));
|
||||
});
|
||||
|
||||
$('.{$htmlPrefix}-enhanced-range-slider').on('input', function() {
|
||||
updateRangeSlider($(this));
|
||||
});
|
||||
$('.{$htmlPrefix}-enhanced-range-slider').each(function() {
|
||||
updateRangeSlider($(this));
|
||||
});
|
||||
updateRangeSlider($(this));
|
||||
});
|
||||
|
||||
// Initialize CodeMirror
|
||||
if (wp.codeEditor) {
|
||||
$('.{$htmlPrefix}-code-editor').each(function() {
|
||||
var textarea = $(this);
|
||||
if (textarea.data('codemirror-initialized')) {
|
||||
return;
|
||||
}
|
||||
if (textarea.data('codemirror-initialized')) { return; }
|
||||
var language = textarea.data('language') || 'css';
|
||||
var mimeType = 'text/' + language;
|
||||
if (language === 'javascript' || language === 'js') mimeType = 'text/javascript';
|
||||
if (language === 'html') mimeType = 'text/html';
|
||||
|
||||
var editorSettings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {};
|
||||
editorSettings.codemirror = _.extend({}, editorSettings.codemirror, {
|
||||
mode: mimeType,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
styleActiveLine: true
|
||||
});
|
||||
editorSettings.codemirror = _.extend({}, editorSettings.codemirror, { mode: mimeType, lineNumbers: true, lineWrapping: true, styleActiveLine: true });
|
||||
|
||||
var editor = wp.codeEditor.initialize(textarea, editorSettings);
|
||||
editor.codemirror.on('change', function() {
|
||||
editor.codemirror.save();
|
||||
textarea.trigger('change');
|
||||
});
|
||||
editor.codemirror.on('change', function() { editor.codemirror.save(); textarea.trigger('change'); });
|
||||
textarea.data('codemirror-initialized', true);
|
||||
});
|
||||
}
|
||||
|
@ -377,34 +344,21 @@ final class AssetManager
|
|||
|
||||
var show = false;
|
||||
switch (operator) {
|
||||
case '==':
|
||||
show = (currentVal == condValue);
|
||||
break;
|
||||
case '!=':
|
||||
show = (currentVal != condValue);
|
||||
break;
|
||||
case 'in':
|
||||
show = Array.isArray(currentVal) ? currentVal.includes(condValue) : condValue.split(',').includes(currentVal);
|
||||
break;
|
||||
case 'not in':
|
||||
show = Array.isArray(currentVal) ? !currentVal.includes(condValue) : !condValue.split(',').includes(currentVal);
|
||||
break;
|
||||
case '==': show = (currentVal == condValue); break;
|
||||
case '!=': show = (currentVal != condValue); break;
|
||||
case 'in': show = Array.isArray(currentVal) ? currentVal.includes(condValue) : condValue.split(',').includes(currentVal); break;
|
||||
case 'not in': show = Array.isArray(currentVal) ? !currentVal.includes(condValue) : !condValue.split(',').includes(currentVal); break;
|
||||
}
|
||||
|
||||
var container = field.closest('tr');
|
||||
if (container.length) {
|
||||
show ? container.show() : container.hide();
|
||||
} else {
|
||||
show ? field.show() : field.hide();
|
||||
}
|
||||
show ? container.show() : container.hide();
|
||||
});
|
||||
}
|
||||
$('body').on('change', 'input, select, textarea', toggleConditionalFields);
|
||||
toggleConditionalFields(); // Initial check on page load.
|
||||
toggleConditionalFields();
|
||||
});
|
||||
JS;
|
||||
|
||||
// phpcs:enable Generic.Files.LineLength
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -414,9 +368,8 @@ JS;
|
|||
*/
|
||||
private function getInlineStyles(): string
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix');
|
||||
|
||||
// Nav should remain prefix-less.
|
||||
// phpcs:disable Generic.Files.LineLength
|
||||
return <<<CSS
|
||||
nav.nav-tab-wrapper .dashicons {
|
||||
|
@ -439,6 +392,15 @@ JS;
|
|||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.{$htmlPrefix}-field-container input[readonly] {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.{$htmlPrefix}-radio-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.{$htmlPrefix}-toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
|
@ -496,16 +458,15 @@ JS;
|
|||
color: white;
|
||||
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
.{$htmlPrefix}-media-field-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
.{$htmlPrefix}-media-preview {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.{$htmlPrefix}-media-preview img {
|
||||
border: 1px solid #ddd;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.07);
|
||||
}
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.{$htmlPrefix}-enhanced-range-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -520,6 +481,6 @@ JS;
|
|||
text-align: center;
|
||||
}
|
||||
CSS;
|
||||
// phpcs:enable Generic.Files.LineLength
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
||||
|
|
229
src/Config.php
Normal file
229
src/Config.php
Normal file
|
@ -0,0 +1,229 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WPTechnix\WPSettings;
|
||||
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
use Traversable;
|
||||
use ArrayIterator;
|
||||
|
||||
/**
|
||||
* Settings Builder Configuration class.
|
||||
*
|
||||
* Provides a configuration container that implements the
|
||||
* ConfigInterface and supports array-style access, iteration,
|
||||
* counting, and JSON serialization.
|
||||
*
|
||||
* This version includes support for dot notation to access nested data.
|
||||
*
|
||||
*/
|
||||
class Config implements ConfigInterface
|
||||
{
|
||||
/**
|
||||
* The configuration data.
|
||||
*
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected array $config;
|
||||
|
||||
/**
|
||||
* Config constructor.
|
||||
*
|
||||
* @param array<string,mixed>|ConfigInterface $config Initial configuration data.
|
||||
*/
|
||||
public function __construct(array|ConfigInterface $config = [])
|
||||
{
|
||||
$this->config = $config instanceof ConfigInterface ?
|
||||
$config->getAll() :
|
||||
$config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Checks if a key exists using dot notation.
|
||||
* e.g., 'database.host'
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
// If the key exists at the top level, return true immediately.
|
||||
if (array_key_exists($key, $this->config)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no dot is present, it's a simple check.
|
||||
if (! str_contains($key, '.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = $this->config;
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (! is_array($current) || ! array_key_exists($segment, $current)) {
|
||||
return false;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
// If the key exists at the top level, return it immediately.
|
||||
if (array_key_exists($key, $this->config)) {
|
||||
return $this->config[$key];
|
||||
}
|
||||
|
||||
// If no dot is present, we know it doesn't exist, return default.
|
||||
if (! str_contains($key, '.')) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$current = $this->config;
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (! is_array($current) || ! array_key_exists($segment, $current)) {
|
||||
return $default;
|
||||
}
|
||||
$current = $current[$segment];
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
if (! str_contains($key, '.')) {
|
||||
$this->config[$key] = $value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$keys = explode('.', $key);
|
||||
$current = &$this->config;
|
||||
|
||||
while (count($keys) > 1) {
|
||||
$segment = array_shift($keys);
|
||||
if (! isset($current[$segment]) || ! is_array($current[$segment])) {
|
||||
$current[$segment] = [];
|
||||
}
|
||||
$current = &$current[$segment];
|
||||
}
|
||||
|
||||
$current[array_shift($keys)] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unset(string $key): void
|
||||
{
|
||||
if (! str_contains($key, '.')) {
|
||||
unset($this->config[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$keys = explode('.', $key);
|
||||
$current = &$this->config;
|
||||
|
||||
while (count($keys) > 1) {
|
||||
$segment = array_shift($keys);
|
||||
if (! isset($current[$segment]) || ! is_array($current[$segment])) {
|
||||
// The path doesn't exist, so there's nothing to unset.
|
||||
return;
|
||||
}
|
||||
$current = &$current[$segment];
|
||||
}
|
||||
|
||||
unset($current[array_shift($keys)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setAll(array $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function deepMerge(array $partial): void
|
||||
{
|
||||
$this->config = array_replace_recursive($this->config, $partial);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @phpstan-return array<string,mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return $this->has((string)$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->get((string)$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->set((string)$offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
$this->unset((string)$offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->config);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ namespace WPTechnix\WPSettings;
|
|||
|
||||
use InvalidArgumentException;
|
||||
use WPTechnix\WPSettings\Interfaces\FieldInterface;
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
|
||||
/**
|
||||
* Creates instances of field objects based on their type.
|
||||
|
@ -54,20 +55,21 @@ final class FieldFactory
|
|||
/**
|
||||
* Creates a field object based on its type.
|
||||
*
|
||||
* @param string $type The field type identifier (e.g., 'text', 'toggle').
|
||||
* @param array<string, mixed> $config The configuration for the field.
|
||||
* @param string $type The field type identifier (e.g., 'text', 'toggle').
|
||||
* @param array<string, mixed>|ConfigInterface $config The configuration for the field.
|
||||
*
|
||||
* @return FieldInterface The instantiated field object.
|
||||
* @throws InvalidArgumentException If the requested field type is not supported.
|
||||
*/
|
||||
public function create(string $type, array $config): FieldInterface
|
||||
public function create(string $type, array|ConfigInterface $config): FieldInterface
|
||||
{
|
||||
if (!isset($this->fieldMap[$type])) {
|
||||
if (! isset($this->fieldMap[$type])) {
|
||||
throw new InvalidArgumentException("Unsupported field type: {$type}");
|
||||
}
|
||||
|
||||
$className = $this->fieldMap[$type];
|
||||
|
||||
return new $className($config);
|
||||
return new $className(new Config($config));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,74 +5,34 @@ declare(strict_types=1);
|
|||
namespace WPTechnix\WPSettings\Fields;
|
||||
|
||||
use WPTechnix\WPSettings\Interfaces\FieldInterface;
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
|
||||
/**
|
||||
* Provides the basic structure and common functionality for all field types.
|
||||
*
|
||||
* @phpstan-type FieldConfig array{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* label: string,
|
||||
* description: string,
|
||||
* default?: mixed,
|
||||
* options?: array<int|string, string>,
|
||||
* attributes?: array<string, scalar>,
|
||||
* sanitize_callback?: callable,
|
||||
* validate_callback?: callable,
|
||||
* conditional?: array{field: string, value: mixed, operator?: string}
|
||||
* }
|
||||
*/
|
||||
abstract class AbstractField implements FieldInterface
|
||||
{
|
||||
/**
|
||||
* The field's configuration properties.
|
||||
*
|
||||
* @var array{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* label: string,
|
||||
* description: string,
|
||||
* default?: mixed,
|
||||
* options?: array<int|string, string>,
|
||||
* attributes?: array<string, scalar>,
|
||||
* sanitize_callback?: callable,
|
||||
* validate_callback?: callable,
|
||||
* conditional?: array{field: string, value: mixed, operator?: string}
|
||||
* }
|
||||
*/
|
||||
protected array $config;
|
||||
|
||||
/**
|
||||
* AbstractField constructor.
|
||||
*
|
||||
* @param array{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* label: string,
|
||||
* description: string,
|
||||
* default?: mixed,
|
||||
* options?: array<int|string, string>,
|
||||
* attributes?: array<string, scalar>,
|
||||
* sanitize_callback?: callable,
|
||||
* validate_callback?: callable,
|
||||
* conditional?: array{field: string, value: mixed, operator?: string}
|
||||
* } $config The field configuration array.
|
||||
* @param ConfigInterface $config The field's configuration properties.
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an HTML attributes string from an array.
|
||||
*
|
||||
* This helper method constructs a valid HTML attribute string from an
|
||||
* associative array, with proper escaping.
|
||||
*
|
||||
* @param array<string, scalar> $attributes The array of attributes (key => value).
|
||||
* @return string The generated HTML attributes string.
|
||||
*/
|
||||
protected function buildAttributesString(array $attributes): string
|
||||
{
|
||||
$attrParts = [];
|
||||
foreach ($attributes as $key => $value) {
|
||||
if (is_bool($value)) {
|
||||
if ($value) {
|
||||
$attrParts[] = esc_attr($key);
|
||||
}
|
||||
} else {
|
||||
$attrParts[] = sprintf('%s="%s"', esc_attr($key), esc_attr((string) $value));
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $attrParts);
|
||||
public function __construct(
|
||||
protected ConfigInterface $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,15 +45,43 @@ abstract class AbstractField implements FieldInterface
|
|||
*/
|
||||
public function getDefaultValue(): mixed
|
||||
{
|
||||
if (array_key_exists('default', $this->config)) {
|
||||
return $this->config['default'];
|
||||
if ($this->config->has('default')) {
|
||||
return $this->config->get('default');
|
||||
}
|
||||
|
||||
$arrayValueFields = [MultiSelectField::class];
|
||||
|
||||
// Fallback for fields that might be arrays.
|
||||
if (in_array(static::class, [MultiSelectField::class], true)) {
|
||||
if (in_array(static::class, $arrayValueFields, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an HTML attributes string from an array.
|
||||
*
|
||||
* This helper method constructs a valid HTML attribute string from an
|
||||
* associative array, with proper escaping.
|
||||
*
|
||||
* @param array<string, scalar> $attributes The array of attributes (key => value).
|
||||
*
|
||||
* @return string The generated HTML attributes string.
|
||||
*/
|
||||
protected function buildAttributesString(array $attributes): string
|
||||
{
|
||||
$attrParts = [];
|
||||
foreach ($attributes as $key => $value) {
|
||||
if (is_bool($value)) {
|
||||
if ($value) {
|
||||
$attrParts[] = esc_attr($key);
|
||||
}
|
||||
} else {
|
||||
$attrParts[] = sprintf('%s="%s"', esc_attr($key), esc_attr((string)$value));
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $attrParts);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,26 +14,26 @@ final class ButtonGroupField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
|
||||
printf(
|
||||
'<input type="hidden" name="%s" id="%s" value="%s" %s />',
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($attributes)
|
||||
);
|
||||
|
||||
printf('<div class="%s-buttongroup-container">', esc_attr($htmlPrefix));
|
||||
|
||||
$options = $this->config['options'] ?? [];
|
||||
$options = $this->config->get('options', []);
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$activeClass = ((string) $value === (string) $optionValue) ? ' active' : '';
|
||||
$activeClass = ((string)$value === (string)$optionValue) ? ' active' : '';
|
||||
printf(
|
||||
'<button type="button" class="%s-buttongroup-option%s" data-value="%s">%s</button>',
|
||||
esc_attr($htmlPrefix),
|
||||
esc_attr($activeClass),
|
||||
esc_attr((string) $optionValue),
|
||||
esc_attr((string)$optionValue),
|
||||
esc_html($optionLabel)
|
||||
);
|
||||
}
|
||||
|
@ -45,10 +45,11 @@ final class ButtonGroupField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
$allowedValues = array_keys($this->config['options'] ?? []);
|
||||
if (in_array((string) $value, $allowedValues, true)) {
|
||||
return (string) $value;
|
||||
$allowedValues = array_keys($this->config->get('options', []));
|
||||
if (in_array((string)$value, $allowedValues, true)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ final class CheckboxField extends AbstractField
|
|||
{
|
||||
printf(
|
||||
'<input type="checkbox" id="%s" name="%s" value="1" %s %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
checked($value, true, false),
|
||||
$this->buildAttributesString($attributes)
|
||||
);
|
||||
|
@ -36,6 +36,6 @@ final class CheckboxField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): bool
|
||||
{
|
||||
return (bool) ($this->config['default'] ?? false);
|
||||
return (bool)($this->config->get('default', false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@ final class CodeField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$language = $this->config['language'] ?? 'css';
|
||||
$language = $this->config->get('language', 'css');
|
||||
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
|
||||
$defaultAttributes = [
|
||||
'rows' => 10,
|
||||
'cols' => 50,
|
||||
'class' => "large-text {$htmlPrefix}-code-editor",
|
||||
'rows' => 10,
|
||||
'cols' => 50,
|
||||
'class' => "large-text {$htmlPrefix}-code-editor",
|
||||
'data-language' => $language
|
||||
];
|
||||
|
||||
|
@ -29,10 +29,10 @@ final class CodeField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<textarea id="%s" name="%s" %s>%s</textarea>',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
$this->buildAttributesString($mergedAttributes),
|
||||
esc_textarea((string) $value)
|
||||
esc_textarea((string)$value)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,6 @@ final class CodeField extends AbstractField
|
|||
public function sanitize(mixed $value): string
|
||||
{
|
||||
// Basic sanitization for code to preserve its structure.
|
||||
return str_replace(["\x00", "\r\n", "\r"], ['', "\n", "\n"], (string) $value);
|
||||
return str_replace(["\x00", "\r\n", "\r"], ['', "\n", "\n"], (string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ final class ColorField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['class' => "{$htmlPrefix}-color-picker"];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
|
||||
printf(
|
||||
'<input type="text" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,8 @@ final class ColorField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
$color = sanitize_hex_color((string) $value);
|
||||
$color = sanitize_hex_color((string)$value);
|
||||
|
||||
return $color ?? $this->getDefaultValue();
|
||||
}
|
||||
|
||||
|
@ -41,6 +42,6 @@ final class ColorField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): string
|
||||
{
|
||||
return $this->config['default'] ?? '#000000';
|
||||
return $this->config->get('default', '#000000');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ final class DateField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['class' => "regular-text {$htmlPrefix}-flatpickr-date", 'readonly' => 'readonly'];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
|
||||
printf(
|
||||
'<input type="text" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ final class DateField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_text_field((string) $value);
|
||||
return sanitize_text_field((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ final class DateTimeField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['class' => "regular-text {$htmlPrefix}-flatpickr-datetime", 'readonly' => 'readonly'];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
|
||||
printf(
|
||||
'<input type="text" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ final class DateTimeField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_text_field((string) $value);
|
||||
return sanitize_text_field((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ final class DescriptionField extends AbstractField
|
|||
{
|
||||
// This field only displays its description, which is handled by the renderer.
|
||||
// It has no input element.
|
||||
if (!empty($this->config['description'])) {
|
||||
echo '<div>' . wp_kses_post($this->config['description']) . '</div>';
|
||||
if (! empty($this->config->get('description'))) {
|
||||
echo '<div>' . wp_kses_post($this->config->get('description')) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ final class EmailField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<input type="email" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ final class EmailField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_email((string) $value);
|
||||
return sanitize_email((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ final class MediaField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$mediaUrl = '';
|
||||
$mediaId = absint($value);
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$mediaUrl = '';
|
||||
$mediaId = absint($value);
|
||||
if ($mediaId > 0) {
|
||||
$mediaUrl = wp_get_attachment_url($mediaId);
|
||||
}
|
||||
|
@ -28,35 +28,35 @@ final class MediaField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<input type="hidden" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($attributes)
|
||||
);
|
||||
printf(
|
||||
'<button type="button" class="button %s-media-upload-button" data-field="%s">%s</button>',
|
||||
esc_attr($htmlPrefix),
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_html__('Select Media', 'default')
|
||||
);
|
||||
|
||||
if (!empty($mediaUrl)) {
|
||||
if (! empty($mediaUrl)) {
|
||||
printf(
|
||||
' <button type="button" class="button %s-media-remove-button" data-field="%s">%s</button>',
|
||||
esc_attr($htmlPrefix),
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_html__('Remove', 'default')
|
||||
);
|
||||
}
|
||||
|
||||
printf('<div class="%s-media-preview">', esc_attr($htmlPrefix));
|
||||
if (!empty($mediaUrl) && wp_attachment_is_image($mediaId)) {
|
||||
if (! empty($mediaUrl) && wp_attachment_is_image($mediaId)) {
|
||||
printf(
|
||||
'<img src="%s" alt="" style="max-width: 150px; height: auto; margin-top: 10px;" />',
|
||||
'<img src="%s" alt="" />',
|
||||
esc_url($mediaUrl)
|
||||
);
|
||||
} elseif (!empty($mediaUrl)) {
|
||||
$file = (string)get_attached_file($mediaId);
|
||||
} elseif (! empty($mediaUrl)) {
|
||||
$file = (string)get_attached_file($mediaId);
|
||||
$fileName = ! empty($file) ? basename($file) : '';
|
||||
printf('<p style="margin-top:10px;"><strong>File:</strong> %s</p>', esc_html($fileName));
|
||||
}
|
||||
|
@ -76,6 +76,6 @@ final class MediaField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): int
|
||||
{
|
||||
return (int) ($this->config['default'] ?? 0);
|
||||
return (int)($this->config->get('default', 0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,24 +14,24 @@ final class MultiSelectField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['multiple' => 'multiple', 'class' => "{$htmlPrefix}-select2-field"];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$selectedValues = is_array($value) ? array_map('strval', $value) : [];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$selectedValues = is_array($value) ? array_map('strval', $value) : [];
|
||||
|
||||
printf(
|
||||
'<select id="%s" name="%s[]" %s>',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
|
||||
$options = $this->config['options'] ?? [];
|
||||
$options = $this->config->get('options', []);
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$selected = in_array((string) $optionValue, $selectedValues, true) ? 'selected="selected"' : '';
|
||||
$selected = in_array((string)$optionValue, $selectedValues, true) ? 'selected="selected"' : '';
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr((string) $optionValue),
|
||||
esc_attr((string)$optionValue),
|
||||
$selected,
|
||||
esc_html($optionLabel)
|
||||
);
|
||||
|
@ -46,16 +46,17 @@ final class MultiSelectField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
if (! is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
$allowedValues = array_keys($this->config['options'] ?? []);
|
||||
$sanitized = [];
|
||||
$allowedValues = array_keys($this->config->get('options', []));
|
||||
$sanitized = [];
|
||||
foreach ($value as $item) {
|
||||
if (in_array((string) $item, $allowedValues, true)) {
|
||||
$sanitized[] = (string) $item;
|
||||
if (in_array((string)$item, $allowedValues, true)) {
|
||||
$sanitized[] = (string)$item;
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
|
@ -66,6 +67,6 @@ final class MultiSelectField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): array
|
||||
{
|
||||
return $this->config['default'] ?? [];
|
||||
return $this->config->get('default', []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ final class NumberField extends AbstractField
|
|||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$defaultAttributes = ['class' => 'regular-text'];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
printf(
|
||||
'<input type="number" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -30,11 +30,12 @@ final class NumberField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): int|float
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
if (! is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
$numericValue = $value + 0; // Cast to number
|
||||
return is_float($numericValue) ? (float) $value : (int) $value;
|
||||
|
||||
return is_float($numericValue) ? (float)$value : (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,6 +43,6 @@ final class NumberField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): int
|
||||
{
|
||||
return (int) ($this->config['default'] ?? 0);
|
||||
return (int)($this->config->get('default', 0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ final class PasswordField extends AbstractField
|
|||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$defaultAttributes = ['class' => 'regular-text'];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
printf(
|
||||
'<input type="password" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ final class PasswordField extends AbstractField
|
|||
{
|
||||
// Passwords should not be altered during sanitization beyond
|
||||
// basic string conversion.
|
||||
return (string) $value;
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,20 +14,23 @@ final class RadioField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$options = $this->config['options'] ?? [];
|
||||
$options = $this->config->get('options', []);
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$radioId = $this->config['id'] . '_' . $optionValue;
|
||||
$radioId = $this->config->get('id') . '_' . sanitize_key($optionValue);
|
||||
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings') ?? '';
|
||||
|
||||
printf(
|
||||
'<label for="%s">
|
||||
'<label for="%s" class="%s-radio-label">
|
||||
<input type="radio" id="%s" name="%s" value="%s" %s %s />
|
||||
%s
|
||||
</label><br />',
|
||||
</label>',
|
||||
esc_attr($radioId),
|
||||
$htmlPrefix,
|
||||
esc_attr($radioId),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $optionValue),
|
||||
checked((string) $value, (string) $optionValue, false),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$optionValue),
|
||||
checked((string)$value, (string)$optionValue, false),
|
||||
$this->buildAttributesString($attributes),
|
||||
esc_html($optionLabel)
|
||||
);
|
||||
|
@ -39,10 +42,11 @@ final class RadioField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
$allowedValues = array_keys($this->config['options'] ?? []);
|
||||
if (in_array((string) $value, $allowedValues, true)) {
|
||||
return (string) $value;
|
||||
$allowedValues = array_keys($this->config->get('options', []));
|
||||
if (in_array((string)$value, $allowedValues, true)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,25 +14,25 @@ final class RangeField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['min' => 0, 'max' => 100, 'step' => 1, 'class' => "{$htmlPrefix}-enhanced-range-slider"];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$min = (int) ($mergedAttributes['min'] ?? 0);
|
||||
$max = (int) ($mergedAttributes['max'] ?? 100);
|
||||
$currentValue = $value ?? $min;
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$min = (int)($mergedAttributes['min'] ?? 0);
|
||||
$max = (int)($mergedAttributes['max'] ?? 100);
|
||||
$currentValue = $value ?? $min;
|
||||
|
||||
printf('<div class="%s-enhanced-range-container">', esc_attr($htmlPrefix));
|
||||
printf(
|
||||
'<input type="range" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $currentValue),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$currentValue),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
printf(
|
||||
'<input type="number" class="%s-range-value-input" value="%s" min="%d" max="%d" readonly />',
|
||||
esc_attr($htmlPrefix),
|
||||
esc_attr((string) $currentValue),
|
||||
esc_attr((string)$currentValue),
|
||||
$min,
|
||||
$max
|
||||
);
|
||||
|
@ -44,11 +44,12 @@ final class RangeField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): int|float
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
if (! is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
$numericValue = $value + 0;
|
||||
return is_float($numericValue) ? (float) $value : (int) $value;
|
||||
|
||||
return is_float($numericValue) ? (float)$value : (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,6 +57,6 @@ final class RangeField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): int
|
||||
{
|
||||
return (int) ($this->config['default'] ?? 0);
|
||||
return (int)($this->config->get('default', 0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,23 +14,31 @@ final class SelectField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
$defaultAttributes = ['class' => "{$htmlPrefix}-select2-field"];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
|
||||
$options = $this->config->get('options', []);
|
||||
|
||||
if (isset($mergedAttributes['data-placeholder'])) {
|
||||
$options = [
|
||||
'' => esc_attr((string)$mergedAttributes['data-placeholder']),
|
||||
...$options
|
||||
];
|
||||
}
|
||||
|
||||
printf(
|
||||
'<select id="%s" name="%s" %s>',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
|
||||
$options = $this->config['options'] ?? [];
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr((string) $optionValue),
|
||||
selected((string) $value, (string) $optionValue, false),
|
||||
esc_attr((string)$optionValue),
|
||||
selected((string)$value, (string)$optionValue, false),
|
||||
esc_html($optionLabel)
|
||||
);
|
||||
}
|
||||
|
@ -42,10 +50,11 @@ final class SelectField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
$allowedValues = array_keys($this->config['options'] ?? []);
|
||||
if (in_array((string) $value, $allowedValues, true)) {
|
||||
return (string) $value;
|
||||
$allowedValues = array_keys($this->config->get('options', []));
|
||||
if (in_array((string)$value, $allowedValues, true)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ final class TextField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<input type="text" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ final class TextField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_text_field((string) $value);
|
||||
return sanitize_text_field((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ final class TextareaField extends AbstractField
|
|||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$defaultAttributes = ['rows' => 5, 'cols' => 50, 'class' => 'large-text'];
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
$mergedAttributes = array_merge($defaultAttributes, $attributes);
|
||||
printf(
|
||||
'<textarea id="%s" name="%s" %s>%s</textarea>',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
$this->buildAttributesString($mergedAttributes),
|
||||
esc_textarea((string) $value)
|
||||
esc_textarea((string)$value)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,6 @@ final class TextareaField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_textarea_field((string) $value);
|
||||
return sanitize_textarea_field((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,10 @@ final class TimeField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
|
||||
$defaultAttributes = [
|
||||
'class' => 'regular-text flatpickr-time',
|
||||
'class' => "regular-text {$htmlPrefix}-flatpickr-time",
|
||||
'readonly' => 'readonly'
|
||||
];
|
||||
|
||||
|
@ -23,9 +25,9 @@ final class TimeField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<input type="text" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -35,6 +37,6 @@ final class TimeField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return sanitize_text_field((string) $value);
|
||||
return sanitize_text_field((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ final class ToggleField extends AbstractField
|
|||
*/
|
||||
public function render(mixed $value, array $attributes): void
|
||||
{
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$htmlPrefix = $this->config->get('htmlPrefix', 'wptechnix-settings');
|
||||
|
||||
printf(
|
||||
'<label class="%s-toggle">
|
||||
|
@ -22,8 +22,8 @@ final class ToggleField extends AbstractField
|
|||
<span class="%s-toggle-slider"></span>
|
||||
</label>',
|
||||
esc_attr($htmlPrefix),
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
checked($value, true, false),
|
||||
$this->buildAttributesString($attributes),
|
||||
esc_attr($htmlPrefix)
|
||||
|
@ -43,6 +43,6 @@ final class ToggleField extends AbstractField
|
|||
*/
|
||||
public function getDefaultValue(): bool
|
||||
{
|
||||
return (bool) ($this->config['default'] ?? false);
|
||||
return (bool)($this->config->get('default', false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ final class UrlField extends AbstractField
|
|||
|
||||
printf(
|
||||
'<input type="url" id="%s" name="%s" value="%s" %s />',
|
||||
esc_attr($this->config['id']),
|
||||
esc_attr($this->config['name']),
|
||||
esc_attr((string) $value),
|
||||
esc_attr($this->config->get('id')),
|
||||
esc_attr($this->config->get('name')),
|
||||
esc_attr((string)$value),
|
||||
$this->buildAttributesString($mergedAttributes)
|
||||
);
|
||||
}
|
||||
|
@ -35,6 +35,6 @@ final class UrlField extends AbstractField
|
|||
*/
|
||||
public function sanitize(mixed $value): string
|
||||
{
|
||||
return esc_url_raw((string) $value);
|
||||
return esc_url_raw((string)$value);
|
||||
}
|
||||
}
|
||||
|
|
84
src/Interfaces/ConfigInterface.php
Normal file
84
src/Interfaces/ConfigInterface.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WPTechnix\WPSettings\Interfaces;
|
||||
|
||||
use ArrayAccess;
|
||||
use JsonSerializable;
|
||||
use IteratorAggregate;
|
||||
use Countable;
|
||||
|
||||
/**
|
||||
* Settings Builder Configuration interface.
|
||||
*
|
||||
* All key-based operations support dot notation for nested config access.
|
||||
*
|
||||
* @extends IteratorAggregate<string,mixed>
|
||||
* @extends ArrayAccess<string,mixed>
|
||||
*/
|
||||
interface ConfigInterface extends
|
||||
ArrayAccess,
|
||||
JsonSerializable,
|
||||
IteratorAggregate,
|
||||
Countable
|
||||
{
|
||||
/**
|
||||
* Check if the given key exists in config.
|
||||
* Supports dot notation for nested keys.
|
||||
*
|
||||
* @param string $key The key to check (dot notation supported).
|
||||
*
|
||||
* @return bool True if exists, false otherwise.
|
||||
*/
|
||||
public function has(string $key): bool;
|
||||
|
||||
/**
|
||||
* Get the value of given config key.
|
||||
* Supports dot notation for nested keys.
|
||||
*
|
||||
* @param string $key Config key (dot notation supported).
|
||||
* @param mixed $default Default value as fallback.
|
||||
*
|
||||
* @return mixed The value of key.
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Get all config values.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function getAll(): array;
|
||||
|
||||
/**
|
||||
* Set the value of given config key.
|
||||
* Supports dot notation for nested keys.
|
||||
*
|
||||
* @param string $key The config key to update (dot notation supported).
|
||||
* @param mixed $value The updated config value.
|
||||
*/
|
||||
public function set(string $key, mixed $value): void;
|
||||
|
||||
/**
|
||||
* Unset the given config key.
|
||||
* Supports dot notation for nested keys.
|
||||
*
|
||||
* @param string $key The config key to unset (dot notation supported).
|
||||
*/
|
||||
public function unset(string $key): void;
|
||||
|
||||
/**
|
||||
* Updates the whole config array.
|
||||
*
|
||||
* @param array<string,mixed> $config Settings Configuration.
|
||||
*/
|
||||
public function setAll(array $config): void;
|
||||
|
||||
/**
|
||||
* Merge the config recursively.
|
||||
*
|
||||
* @param array<string,mixed> $partial Settings Configuration.
|
||||
*/
|
||||
public function deepMerge(array $partial): void;
|
||||
}
|
|
@ -14,7 +14,7 @@ interface FieldInterface
|
|||
*
|
||||
* This method is responsible for echoing the complete HTML for the form element.
|
||||
*
|
||||
* @param mixed $value The current value of the field.
|
||||
* @param mixed $value The current value of the field.
|
||||
* @param array<string, string|int|bool> $attributes Additional HTML attributes for the field.
|
||||
*/
|
||||
public function render(mixed $value, array $attributes): void;
|
||||
|
@ -26,6 +26,7 @@ interface FieldInterface
|
|||
* before being persisted to the database.
|
||||
*
|
||||
* @param mixed $value The raw input value to be sanitized.
|
||||
*
|
||||
* @return mixed The sanitized value.
|
||||
*/
|
||||
public function sanitize(mixed $value): mixed;
|
||||
|
|
|
@ -5,79 +5,118 @@ declare(strict_types=1);
|
|||
namespace WPTechnix\WPSettings\Interfaces;
|
||||
|
||||
/**
|
||||
* Defines the contract for a settings page builder.
|
||||
* Defines the public contract for a settings page builder.
|
||||
*/
|
||||
interface SettingsInterface
|
||||
{
|
||||
/**
|
||||
* Sets or overrides the main page title.
|
||||
* Sets the main title of the settings page (the `<h1>` tag).
|
||||
*
|
||||
* @param string $pageTitle The new title for the settings page.
|
||||
* @return static
|
||||
* @param string $pageTitle The main title for the settings page.
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function setPageTitle(string $pageTitle): static;
|
||||
|
||||
/**
|
||||
* Sets or overrides the menu title.
|
||||
* Sets the title displayed in the WordPress admin menu.
|
||||
*
|
||||
* @param string $menuTitle The new title for the admin menu item.
|
||||
* @return static
|
||||
* @param string $menuTitle The title for the admin menu item.
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function setMenuTitle(string $menuTitle): static;
|
||||
|
||||
/**
|
||||
* Adds a tab for organizing settings sections.
|
||||
* Sets the required capability to view and save the settings page.
|
||||
*
|
||||
* @param string $id The unique identifier for the tab.
|
||||
* @param string $title The text to display on the tab.
|
||||
* @param string $icon Optional. A Dashicon class to display next to the title.
|
||||
* @return static
|
||||
* @param string $capability The WordPress capability string (e.g., 'manage_options').
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function setCapability(string $capability): static;
|
||||
|
||||
/**
|
||||
* Sets the parent menu page slug under which this settings page will appear.
|
||||
*
|
||||
* @param string $parentSlug The slug of the parent menu (e.g., 'options-general.php', 'themes.php').
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function setParentSlug(string $parentSlug): static;
|
||||
|
||||
/**
|
||||
* Adds a navigation tab to the settings page.
|
||||
* This automatically enables the tabbed interface.
|
||||
*
|
||||
* @param string $id A unique identifier for the tab.
|
||||
* @param string $title The visible title of the tab.
|
||||
* @param string $icon (Optional) A Dashicons class for an icon (e.g., 'dashicons-admin-generic').
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function addTab(string $id, string $title, string $icon = ''): static;
|
||||
|
||||
/**
|
||||
* Adds a settings section to the page.
|
||||
* Adds a settings section to group related fields.
|
||||
*
|
||||
* @param string $id The unique identifier for the section.
|
||||
* @param string $title The title displayed for the section.
|
||||
* @param string $description Optional. A description displayed below the section title.
|
||||
* @param string $tabId Optional. The ID of the tab this section should appear under.
|
||||
* @return static
|
||||
* @param string $id A unique identifier for the section.
|
||||
* @param string $title The visible title of the section (an `<h2>` tag).
|
||||
* @param string $description (Optional) A short description displayed below the section title.
|
||||
* @param string $tabId (Optional) The ID of the tab this section belongs to. Required for tabbed interfaces.
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function addSection(string $id, string $title, string $description = '', string $tabId = ''): static;
|
||||
|
||||
/**
|
||||
* Adds a field to a section.
|
||||
* Adds a setting field to a section.
|
||||
*
|
||||
* @param string $id The unique identifier for the field.
|
||||
* @param string $sectionId The ID of the section this field belongs to.
|
||||
* @param string $type The field type (e.g., 'text', 'toggle', 'code').
|
||||
* @param string $label The label displayed for the field.
|
||||
* @param array<string, mixed> $args Optional. An array of additional arguments.
|
||||
* @return static
|
||||
* @param string $id A unique identifier for the field, used as the key in the options array.
|
||||
* @param string $sectionId The ID of the section this field belongs to.
|
||||
* @param string $type The type of field (e.g., 'text', 'toggle', 'select').
|
||||
* @param string $label The label displayed for the field.
|
||||
* @param array<string, mixed> $args (Optional) Additional arguments for the field, such as 'default',
|
||||
* 'description', 'options', 'attributes', etc.
|
||||
*
|
||||
* @return static Provides a fluent interface.
|
||||
*/
|
||||
public function addField(string $id, string $sectionId, string $type, string $label, array $args = []): static;
|
||||
public function addField(
|
||||
string $id,
|
||||
string $sectionId,
|
||||
string $type,
|
||||
string $label,
|
||||
array $args = []
|
||||
): static;
|
||||
|
||||
/**
|
||||
* Initializes the settings page and hooks all components into WordPress.
|
||||
* Hooks the settings framework into the appropriate WordPress actions.
|
||||
* This method must be called to activate the settings page and make it appear.
|
||||
*
|
||||
* This method must be called after all configuration is complete.
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void;
|
||||
|
||||
/**
|
||||
* Gets the WordPress option name where settings are stored.
|
||||
* Gets a saved option value from the database.
|
||||
*
|
||||
* @return string The option name.
|
||||
*/
|
||||
public function getOptionName(): string;
|
||||
|
||||
/**
|
||||
* Retrieves a setting's value for this settings page.
|
||||
* This is the primary method for retrieving a field's current value for rendering.
|
||||
* It intelligently falls back to the field's configured 'default' value if no
|
||||
* saved value exists in the database.
|
||||
*
|
||||
* @param string $key The unique key of the setting to retrieve.
|
||||
* @param mixed $default A fallback value to return if the setting is not found.
|
||||
* @return mixed The stored setting value, or the default if not found.
|
||||
* @param string $key The specific option key (field ID) to retrieve.
|
||||
* @param mixed|null $default A final fallback value if no saved option or field default is found.
|
||||
*
|
||||
* @return mixed The saved value, the field's default value, or the provided default.
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Gets the main option name used in the database.
|
||||
*
|
||||
* This is the top-level key for the array stored in the `wp_options` table.
|
||||
*
|
||||
* @return string The name of the option array.
|
||||
*/
|
||||
public function getOptionName(): string;
|
||||
}
|
||||
|
|
|
@ -4,41 +4,26 @@ declare(strict_types=1);
|
|||
|
||||
namespace WPTechnix\WPSettings;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
|
||||
/**
|
||||
* Handles all HTML output for the settings page.
|
||||
*
|
||||
* This class isolates the presentation logic from the business logic. It is
|
||||
* responsible for rendering the main page wrapper, navigation tabs, and the
|
||||
* settings form itself.
|
||||
*
|
||||
* @noinspection HtmlUnknownAttribute
|
||||
*/
|
||||
final class PageRenderer
|
||||
{
|
||||
/**
|
||||
* The main settings configuration array.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* The FieldFactory instance for creating field objects.
|
||||
*
|
||||
* @var FieldFactory
|
||||
*/
|
||||
private FieldFactory $fieldFactory;
|
||||
|
||||
/**
|
||||
* PageRenderer constructor.
|
||||
*
|
||||
* @param array<string, mixed> $config The main settings configuration.
|
||||
* @param FieldFactory $fieldFactory The factory for creating field objects.
|
||||
* @param ConfigInterface $config The shared configuration object.
|
||||
* @param FieldFactory $fieldFactory The factory for creating field objects.
|
||||
* @param Settings $settings The main Settings instance to access get().
|
||||
*/
|
||||
public function __construct(array $config, FieldFactory $fieldFactory)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->fieldFactory = $fieldFactory;
|
||||
public function __construct(
|
||||
protected ConfigInterface $config,
|
||||
protected FieldFactory $fieldFactory,
|
||||
protected Settings $settings
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,28 +34,39 @@ final class PageRenderer
|
|||
*/
|
||||
public function renderPage(): void
|
||||
{
|
||||
if (!empty($this->config['capability']) && !current_user_can($this->config['capability'])) {
|
||||
wp_die(esc_html($this->config['labels']['noPermission'] ?? 'Permission denied.'));
|
||||
if (! current_user_can($this->config->get('capability'))) {
|
||||
wp_die(
|
||||
esc_html($this->config->get('labels.noPermission', 'Permission Denied.'))
|
||||
);
|
||||
}
|
||||
|
||||
$activeTab = $this->getActiveTab();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html($this->config['pageTitle']); ?></h1>
|
||||
<h1><?php
|
||||
echo esc_html($this->config->get('pageTitle')); ?></h1>
|
||||
|
||||
<?php if (!empty($this->config['useTabs']) && !empty($this->config['tabs'])) : ?>
|
||||
<?php $this->renderTabs($activeTab); ?>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
settings_errors();
|
||||
?>
|
||||
|
||||
<?php
|
||||
if (false !== $this->config->get('useTabs') && $this->config->has('tabs')) : ?>
|
||||
<?php
|
||||
$this->renderTabs(); ?>
|
||||
<?php
|
||||
endif; ?>
|
||||
|
||||
<!--suppress HtmlUnknownTarget -->
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields($this->config['optionGroup']);
|
||||
settings_fields($this->config->get('optionGroup'));
|
||||
|
||||
if (!empty($this->config['useTabs']) && !empty($activeTab)) {
|
||||
$this->renderSectionsForTab($activeTab);
|
||||
if (! empty($this->config->get('useTabs'))) {
|
||||
$activeTab = $this->config->get('activeTab');
|
||||
printf('<input type="hidden" name="tab" value="%s" />', $activeTab);
|
||||
$this->renderSections();
|
||||
} else {
|
||||
do_settings_sections($this->config['pageSlug']);
|
||||
do_settings_sections($this->config->get('pageSlug'));
|
||||
}
|
||||
|
||||
submit_button();
|
||||
|
@ -87,90 +83,104 @@ final class PageRenderer
|
|||
* that the rendered field's HTML has the correct CSS classes.
|
||||
*
|
||||
* @param array<string, mixed> $args Arguments passed from `add_settings_field`.
|
||||
* @return void
|
||||
*/
|
||||
public function renderField(array $args): void
|
||||
{
|
||||
$fieldId = $args['id'] ?? '';
|
||||
$fieldConfig = $this->config['fields'][$fieldId] ?? null;
|
||||
|
||||
if (empty($fieldConfig)) {
|
||||
return; // Safety check.
|
||||
$fieldId = $args['id'] ?? null;
|
||||
if (empty($fieldId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlPrefix = $this->config['htmlPrefix'] ?? 'wptechnix-settings';
|
||||
$fieldConfig['htmlPrefix'] = $htmlPrefix; // Pass the prefix so that later we can use in fields.
|
||||
$fieldConfig = $this->config->get("fields.{$fieldId}");
|
||||
|
||||
$options = get_option($this->config['optionName'], []);
|
||||
$value = $options[$fieldId] ?? $fieldConfig['default'] ?? null;
|
||||
if (empty($fieldConfig) || ! is_array($fieldConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlPrefix = $this->config->get('htmlPrefix');
|
||||
|
||||
$fieldConfig['htmlPrefix'] = $htmlPrefix;
|
||||
|
||||
try {
|
||||
$field = $this->fieldFactory->create($fieldConfig['type'], $fieldConfig);
|
||||
$fieldObject = $this->fieldFactory->create($fieldConfig['type'], $fieldConfig);
|
||||
|
||||
$value = $this->settings->get($fieldId, $fieldObject->getDefaultValue());
|
||||
|
||||
$fieldAttributes = $fieldConfig['attributes'] ?? [];
|
||||
if (! is_array($fieldAttributes)) {
|
||||
$fieldAttributes = [];
|
||||
}
|
||||
|
||||
$conditionalAttr = '';
|
||||
if (!empty($fieldConfig['conditional'])) {
|
||||
$cond = $fieldConfig['conditional'];
|
||||
if (! empty($fieldConfig['conditional'])) {
|
||||
$cond = $fieldConfig['conditional'];
|
||||
$conditionalAttr = sprintf(
|
||||
'data-conditional="%s" data-conditional-value="%s" data-conditional-operator="%s"',
|
||||
esc_attr($cond['field'] ?? ''),
|
||||
esc_attr((string) ($cond['value'] ?? '')),
|
||||
esc_attr((string)($cond['value'] ?? '')),
|
||||
esc_attr($cond['operator'] ?? '==')
|
||||
);
|
||||
}
|
||||
|
||||
printf('<div class="%s-field-container" %s>', esc_attr($htmlPrefix), $conditionalAttr);
|
||||
$field->render($value, $fieldConfig['attributes'] ?? []);
|
||||
|
||||
if (!empty($fieldConfig['description']) && 'description' !== $fieldConfig['type']) {
|
||||
$fieldObject->render($value, $fieldAttributes);
|
||||
|
||||
if (! empty($fieldConfig['description']) && 'description' !== $fieldConfig['type']) {
|
||||
echo '<p class="description">' . wp_kses_post($fieldConfig['description']) . '</p>';
|
||||
}
|
||||
echo '</div>';
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
echo '<p><strong>Error:</strong> ' . esc_html($e->getMessage()) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the navigation tabs for the settings page.
|
||||
*
|
||||
* @param string $activeTab The slug of the currently active tab.
|
||||
*/
|
||||
private function renderTabs(string $activeTab): void
|
||||
private function renderTabs(): void
|
||||
{
|
||||
echo '<nav class="nav-tab-wrapper" style="margin-bottom: 20px;">';
|
||||
foreach ($this->config['tabs'] as $tabId => $tab) {
|
||||
$url = add_query_arg(['page' => $this->config['pageSlug'], 'tab' => $tabId]);
|
||||
$class = 'nav-tab' . ($activeTab === $tabId ? ' nav-tab-active' : '');
|
||||
$htmlPrefix = $this->config->get('htmlPrefix');
|
||||
$activeTab = $this->config->get('activeTab'); // Correct: Read from config.
|
||||
|
||||
$classes = implode(' ', [
|
||||
'nav-tab-wrapper',
|
||||
$htmlPrefix . '-nav-tab-wrapper'
|
||||
]);
|
||||
|
||||
echo '<nav class="' . esc_attr($classes) . '">';
|
||||
foreach ($this->config->get('tabs', []) as $tabId => $tab) {
|
||||
$url = add_query_arg(['page' => $this->config->get('pageSlug'), 'tab' => $tabId]);
|
||||
$class = 'nav-tab' . ($tabId === $activeTab ? ' nav-tab-active' : '');
|
||||
$icon = ! empty($tab['icon']) ? '<span class="dashicons ' . esc_attr($tab['icon']) . '"></span>' : '';
|
||||
printf(
|
||||
'<a href="%s" class="%s">%s %s</a>',
|
||||
'<a href="%s" class="%s">%s%s</a>',
|
||||
esc_url($url),
|
||||
esc_attr($class),
|
||||
!empty($tab['icon']) ?
|
||||
'<span class="dashicons ' . esc_attr($tab['icon']) . '" style="margin-right: 5px;"></span>'
|
||||
: '',
|
||||
$icon,
|
||||
esc_html($tab['title'])
|
||||
);
|
||||
}
|
||||
echo '</nav>';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders all settings sections associated with a specific tab.
|
||||
*
|
||||
* @param string $activeTab The slug of the currently active tab.
|
||||
* Renders all settings sections associated with active tab.
|
||||
*/
|
||||
private function renderSectionsForTab(string $activeTab): void
|
||||
private function renderSections(): void
|
||||
{
|
||||
global $wp_settings_sections, $wp_settings_fields;
|
||||
|
||||
$page = $this->config['pageSlug'];
|
||||
$page = $this->config->get('pageSlug');
|
||||
$activeTab = $this->config->get('activeTab'); // Correct: Read from config.
|
||||
|
||||
if (empty($wp_settings_sections[$page])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((array) $wp_settings_sections[$page] as $section) {
|
||||
$sectionTab = $this->config['sections'][$section['id']]['tab'] ?? '';
|
||||
foreach ((array)$wp_settings_sections[$page] as $section) {
|
||||
$sectionTab = $this->config->get("sections.{$section['id']}.tab", '');
|
||||
if ($sectionTab !== $activeTab) {
|
||||
continue;
|
||||
}
|
||||
|
@ -183,35 +193,11 @@ final class PageRenderer
|
|||
call_user_func($section['callback'], $section);
|
||||
}
|
||||
|
||||
if (
|
||||
!isset($wp_settings_fields) ||
|
||||
!isset($wp_settings_fields[$page]) ||
|
||||
!isset($wp_settings_fields[$page][$section['id']])
|
||||
) {
|
||||
continue;
|
||||
if (isset($wp_settings_fields[$page][$section['id']])) {
|
||||
echo '<table class="form-table" role="presentation">';
|
||||
do_settings_fields($page, $section['id']);
|
||||
echo '</table>';
|
||||
}
|
||||
echo '<table class="form-table" role="presentation">';
|
||||
do_settings_fields($page, $section['id']);
|
||||
echo '</table>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the currently active tab from the URL query string.
|
||||
*
|
||||
* @return string The slug of the active tab, or an empty string if none.
|
||||
*/
|
||||
private function getActiveTab(): string
|
||||
{
|
||||
if (empty($this->config['useTabs']) || empty($this->config['tabs'])) {
|
||||
return '';
|
||||
}
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$activeTab = sanitize_text_field($_GET['tab'] ?? '');
|
||||
if (empty($activeTab) || !isset($this->config['tabs'][$activeTab])) {
|
||||
return array_key_first($this->config['tabs']) ?? '';
|
||||
}
|
||||
return $activeTab;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,103 +5,148 @@ declare(strict_types=1);
|
|||
namespace WPTechnix\WPSettings;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use WPTechnix\WPSettings\Interfaces\ConfigInterface;
|
||||
|
||||
/**
|
||||
* Sanitizes the settings array before it is saved to the database.
|
||||
* Sanitizes settings before they are saved to the database.
|
||||
*
|
||||
* This class ensures that all data conforms to the expected format and is
|
||||
* safe for storage. It iterates through all registered fields and applies
|
||||
* the appropriate sanitization logic.
|
||||
* This class is state-aware. When sanitizing a form submission (especially
|
||||
* from a tabbed interface), it merges the newly submitted values with the
|
||||
* existing saved values to prevent data loss from other tabs.
|
||||
*/
|
||||
final class Sanitizer
|
||||
{
|
||||
/**
|
||||
* The full settings configuration array.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* The FieldFactory instance for creating field objects.
|
||||
*
|
||||
* @var FieldFactory
|
||||
*/
|
||||
private FieldFactory $fieldFactory;
|
||||
|
||||
/**
|
||||
* Sanitizer constructor.
|
||||
*
|
||||
* @param array<string, mixed> $config The settings configuration.
|
||||
* @param FieldFactory $fieldFactory The factory for creating field objects.
|
||||
* @param ConfigInterface $config The shared configuration object.
|
||||
* @param FieldFactory $fieldFactory The factory for creating field objects.
|
||||
*/
|
||||
public function __construct(array $config, FieldFactory $fieldFactory)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->fieldFactory = $fieldFactory;
|
||||
public function __construct(
|
||||
protected ConfigInterface $config,
|
||||
protected FieldFactory $fieldFactory
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the entire settings array.
|
||||
* Sanitizes the settings array by merging new input with existing options.
|
||||
*
|
||||
* This is the main callback for the 'sanitize_callback' argument in
|
||||
* `register_setting`. It processes the raw input from the $_POST array.
|
||||
*
|
||||
* @param mixed $input The raw input from the form submission.
|
||||
* @return array<string, mixed> The sanitized settings array ready for saving.
|
||||
* @param mixed $input The raw input from the form submission (from `$_POST`).
|
||||
*
|
||||
* @return array<string, mixed> The complete, sanitized settings array ready for saving.
|
||||
*/
|
||||
public function sanitize(mixed $input): array
|
||||
{
|
||||
if (!is_array($input)) {
|
||||
return [];
|
||||
// 1. Fetch all previously saved options from the database.
|
||||
// This is the base we'll be merging the new values into.
|
||||
$optionName = $this->config->get('optionName');
|
||||
$oldOptions = get_option($optionName, []);
|
||||
$oldOptions = is_array($oldOptions) ? $oldOptions : [];
|
||||
|
||||
// If the submitted data isn't an array, it's invalid. Return the old
|
||||
// options to prevent data loss and show an error.
|
||||
if (! is_array($input)) {
|
||||
add_settings_error(
|
||||
$this->config->get('optionGroup'),
|
||||
'invalid_input_type',
|
||||
'Settings data received was not in the expected format. No changes were saved.',
|
||||
'error'
|
||||
);
|
||||
|
||||
return $oldOptions;
|
||||
}
|
||||
|
||||
$sanitized = [];
|
||||
$fields = $this->config['fields'] ?? [];
|
||||
// 2. Determine which fields we need to process from this submission.
|
||||
$fieldsToProcess = $this->getFieldsToProcess();
|
||||
$newValues = [];
|
||||
|
||||
foreach ($fields as $fieldId => $fieldConfig) {
|
||||
// Description fields have no value and should be skipped.
|
||||
if ('description' === $fieldConfig['type']) {
|
||||
continue;
|
||||
foreach ($fieldsToProcess as $fieldId => $fieldConfig) {
|
||||
$rawValue = $input[$fieldId] ?? null;
|
||||
$fieldType = $fieldConfig['type'] ?? 'text';
|
||||
|
||||
if ('description' === $fieldType) {
|
||||
continue; // Description-only fields are not saved.
|
||||
}
|
||||
|
||||
$rawValue = $input[$fieldId] ?? null;
|
||||
|
||||
try {
|
||||
$field = $this->fieldFactory->create($fieldConfig['type'], $fieldConfig);
|
||||
$field = $this->fieldFactory->create($fieldType, $fieldConfig);
|
||||
$defaultValue = $field->getDefaultValue();
|
||||
|
||||
// If value is not submitted, use default.
|
||||
if (null === $rawValue) {
|
||||
$sanitized[$fieldId] = $defaultValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the field's specific sanitization method.
|
||||
// If a value is not submitted (e.g., an unchecked checkbox),
|
||||
// it will be null. We should process it to get its "off" state (e.g., '0' or '').
|
||||
$sanitizedValue = $field->sanitize($rawValue);
|
||||
|
||||
// Apply custom validation callback if it exists.
|
||||
// Apply custom validation if it exists.
|
||||
if (isset($fieldConfig['validate_callback']) && is_callable($fieldConfig['validate_callback'])) {
|
||||
if (!call_user_func($fieldConfig['validate_callback'], $sanitizedValue)) {
|
||||
// If validation fails, revert to the default value.
|
||||
$sanitizedValue = $defaultValue;
|
||||
if (! call_user_func($fieldConfig['validate_callback'], $sanitizedValue)) {
|
||||
$sanitizedValue = $defaultValue; // Revert on validation fail.
|
||||
|
||||
$errorMessageTemplate = $this->config->get(
|
||||
'labels.validationError',
|
||||
'Invalid value for %s. Reverted to default.'
|
||||
);
|
||||
$errorMessage = sprintf($errorMessageTemplate, $fieldConfig['label'] ?? $fieldId);
|
||||
|
||||
add_settings_error(
|
||||
$this->config['optionGroup'],
|
||||
$this->config->get('optionGroup'),
|
||||
'validation_error_' . $fieldId,
|
||||
'Invalid value provided for ' . $fieldConfig['label'] . '. Reverted to default.',
|
||||
$errorMessage,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$sanitized[$fieldId] = $sanitizedValue;
|
||||
// Store the processed value.
|
||||
$newValues[$fieldId] = $sanitizedValue;
|
||||
} catch (InvalidArgumentException) {
|
||||
// This should not happen if types are validated on creation.
|
||||
// For safety, we use the default value.
|
||||
$sanitized[$fieldId] = $fieldConfig['default'] ?? '';
|
||||
// Failsafe if field type is somehow invalid.
|
||||
$newValues[$fieldId] = $fieldConfig['default'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
// 3. Merge the newly sanitized values into the old options and return.
|
||||
// This preserves all settings from other tabs.
|
||||
return array_merge($oldOptions, $newValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which fields should be processed in the current request.
|
||||
*
|
||||
* If tabs are enabled, it returns only the fields for the active tab.
|
||||
* Otherwise, it returns all registered fields.
|
||||
*
|
||||
* @return array<string, mixed> An array of field configurations to process.
|
||||
*/
|
||||
private function getFieldsToProcess(): array
|
||||
{
|
||||
$allFields = $this->config->get('fields', []);
|
||||
|
||||
// If not using tabs, process all fields.
|
||||
if (empty($this->config->get('useTabs'))) {
|
||||
return $allFields;
|
||||
}
|
||||
|
||||
$activeTab = $this->config->get('activeTab');
|
||||
|
||||
if (empty($activeTab)) {
|
||||
// Failsafe: if tabs are on but no tab is active, process nothing
|
||||
// to be safe.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter all fields to get only those belonging to the active tab.
|
||||
return array_filter($allFields, function ($field) use ($activeTab) {
|
||||
$sectionId = $field['section'] ?? null;
|
||||
if (empty($sectionId)) {
|
||||
return false;
|
||||
}
|
||||
// A field belongs to a tab via its section's 'tab' property.
|
||||
$sectionTab = $this->config->get("sections.{$sectionId}.tab");
|
||||
|
||||
return $sectionTab === $activeTab;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
345
src/Settings.php
345
src/Settings.php
|
@ -9,84 +9,122 @@ use WPTechnix\WPSettings\Interfaces\SettingsInterface;
|
|||
|
||||
/**
|
||||
* A fluent builder for creating and managing WordPress admin settings pages.
|
||||
*
|
||||
* @phpstan-type SettingsConfig array{
|
||||
* optionName: string,
|
||||
* optionGroup: string,
|
||||
* pageSlug: string,
|
||||
* parentSlug: string,
|
||||
* capability: string,
|
||||
* pageTitle: string,
|
||||
* menuTitle: string,
|
||||
* useTabs: bool,
|
||||
* htmlPrefix: string,
|
||||
* activeTab: ?string,
|
||||
* tabs: array<string, array{
|
||||
* title: string,
|
||||
* icon: string
|
||||
* }>,
|
||||
* sections: array<string, array{
|
||||
* title: string,
|
||||
* description: string,
|
||||
* tab: string
|
||||
* }>,
|
||||
* fields: array<string, array<string, mixed>>,
|
||||
* assetPackages: array<string, mixed>,
|
||||
* labels: array<string, string>
|
||||
* }
|
||||
*/
|
||||
class Settings implements SettingsInterface
|
||||
{
|
||||
/**
|
||||
* The settings configuration array.
|
||||
* The central configuration object for the settings page.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
* @var Config
|
||||
*/
|
||||
private array $config = [
|
||||
'tabs' => [],
|
||||
'sections' => [],
|
||||
'fields' => [],
|
||||
];
|
||||
protected Config $config;
|
||||
|
||||
/**
|
||||
* The FieldFactory instance.
|
||||
* Instance of the factory responsible for creating field objects.
|
||||
*
|
||||
* @var FieldFactory
|
||||
*/
|
||||
private FieldFactory $fieldFactory;
|
||||
protected FieldFactory $fieldFactory;
|
||||
|
||||
/**
|
||||
* The AssetManager instance.
|
||||
* Instance of the asset manager for enqueueing scripts and styles.
|
||||
*
|
||||
* @var AssetManager
|
||||
*/
|
||||
private AssetManager $assetManager;
|
||||
protected AssetManager $assetManager;
|
||||
|
||||
/**
|
||||
* A cached copy of the settings options array from the database.
|
||||
* Instance of the page renderer responsible for all HTML output.
|
||||
*
|
||||
* @var PageRenderer
|
||||
*/
|
||||
protected PageRenderer $pageRenderer;
|
||||
|
||||
/**
|
||||
* A cache for the options loaded from the database for the current request.
|
||||
* This prevents multiple `get_option()` calls on the same page load.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
private ?array $options = null;
|
||||
protected ?array $savedOptions = null;
|
||||
|
||||
/**
|
||||
* Settings constructor.
|
||||
*
|
||||
* @param string $pageSlug The unique settings page slug.
|
||||
* @param string|null $pageTitle Optional. The title for the settings page. Defaults to "Settings".
|
||||
* @param string|null $menuTitle Optional. The title for the admin menu. Defaults to the page title.
|
||||
* @param array<string, mixed> $options Optional configuration overrides.
|
||||
* Initializes the settings framework with essential parameters and default configurations.
|
||||
*
|
||||
* @param string $optionName The name of the option to be stored in the wp_options table.
|
||||
* @param string $pageSlug The unique slug for the settings page URL.
|
||||
* @param array<string, mixed> $options Optional configuration overrides.
|
||||
*
|
||||
* @throws InvalidArgumentException If optionName or pageSlug are empty.
|
||||
*/
|
||||
public function __construct(
|
||||
string $pageSlug,
|
||||
?string $pageTitle = null,
|
||||
?string $menuTitle = null,
|
||||
array $options = []
|
||||
) {
|
||||
public function __construct(string $optionName, string $pageSlug, array $options = [])
|
||||
{
|
||||
if (empty($optionName)) {
|
||||
throw new InvalidArgumentException('Option name cannot be empty.');
|
||||
}
|
||||
if (empty($pageSlug)) {
|
||||
throw new InvalidArgumentException('Page slug cannot be empty.');
|
||||
}
|
||||
|
||||
$this->fieldFactory = new FieldFactory();
|
||||
$this->assetManager = new AssetManager();
|
||||
|
||||
$finalPageTitle = $pageTitle ?? __('Settings', 'default');
|
||||
$finalMenuTitle = $menuTitle ?? $finalPageTitle;
|
||||
|
||||
$this->config = array_replace_recursive(
|
||||
[
|
||||
'pageSlug' => $pageSlug,
|
||||
'pageTitle' => $finalPageTitle,
|
||||
'menuTitle' => $finalMenuTitle,
|
||||
'capability' => 'manage_options',
|
||||
'parentSlug' => 'options-general.php',
|
||||
'useTabs' => false,
|
||||
'optionName' => $pageSlug . '_settings',
|
||||
'optionGroup' => $pageSlug . '_settings_group',
|
||||
'htmlPrefix' => 'wptechnix-settings', // no underscore or dash in end.
|
||||
'labels' => [
|
||||
'noPermission' => __('You do not have permission to access this page.', 'default'),
|
||||
'selectMedia' => __('Select Media', 'default'),
|
||||
'remove' => __('Remove', 'default'),
|
||||
],
|
||||
/** @phpstan-var SettingsConfig $defaults */
|
||||
$defaults = [
|
||||
'optionName' => $optionName,
|
||||
'optionGroup' => $optionName . '_group',
|
||||
'pageSlug' => $pageSlug,
|
||||
'parentSlug' => 'options-general.php',
|
||||
'capability' => 'manage_options',
|
||||
'pageTitle' => 'Settings',
|
||||
'menuTitle' => 'Settings',
|
||||
'useTabs' => false,
|
||||
'htmlPrefix' => 'wptechnix-settings',
|
||||
'tabs' => [],
|
||||
'activeTab' => null,
|
||||
'sections' => [],
|
||||
'fields' => [],
|
||||
'assetPackages' => $this->getDefaultAssetPackages(),
|
||||
'labels' => [
|
||||
'noPermission' => 'You do not have permission to access this page.',
|
||||
'addMediaTitle' => __('Add media', 'default'),
|
||||
'selectMediaText' => __('Select', 'default'),
|
||||
'removeMediaText' => __('Remove', 'default'),
|
||||
/* translators: %s: Field label */
|
||||
'validationError' => 'Invalid value for %s. The setting has been reverted to its default value.',
|
||||
],
|
||||
$options
|
||||
);
|
||||
];
|
||||
|
||||
$this->config = new Config($defaults);
|
||||
$this->config->deepMerge($options);
|
||||
|
||||
$this->fieldFactory = new FieldFactory();
|
||||
$this->assetManager = new AssetManager($this->config);
|
||||
$this->pageRenderer = new PageRenderer($this->config, $this->fieldFactory, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,7 +132,8 @@ class Settings implements SettingsInterface
|
|||
*/
|
||||
public function setPageTitle(string $pageTitle): static
|
||||
{
|
||||
$this->config['pageTitle'] = $pageTitle;
|
||||
$this->config->set('pageTitle', $pageTitle);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -103,7 +142,28 @@ class Settings implements SettingsInterface
|
|||
*/
|
||||
public function setMenuTitle(string $menuTitle): static
|
||||
{
|
||||
$this->config['menuTitle'] = $menuTitle;
|
||||
$this->config->set('menuTitle', $menuTitle);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setCapability(string $capability): static
|
||||
{
|
||||
$this->config->set('capability', $capability);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setParentSlug(string $parentSlug): static
|
||||
{
|
||||
$this->config->set('parentSlug', $parentSlug);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -112,8 +172,10 @@ class Settings implements SettingsInterface
|
|||
*/
|
||||
public function addTab(string $id, string $title, string $icon = ''): static
|
||||
{
|
||||
$this->config['tabs'][$id] = ['title' => $title, 'icon' => $icon];
|
||||
$this->config['useTabs'] = true;
|
||||
$id = sanitize_key($id);
|
||||
$this->config->set("tabs.{$id}", ['title' => $title, 'icon' => $icon]);
|
||||
$this->config->set('useTabs', true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -122,38 +184,40 @@ class Settings implements SettingsInterface
|
|||
*/
|
||||
public function addSection(string $id, string $title, string $description = '', string $tabId = ''): static
|
||||
{
|
||||
$this->config['sections'][$id] = ['title' => $title, 'description' => $description, 'tab' => $tabId];
|
||||
$id = sanitize_key($id);
|
||||
$this->config->set("sections.{$id}", [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'tab' => $tabId,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function addField(
|
||||
string $id,
|
||||
string $sectionId,
|
||||
string $type,
|
||||
string $label,
|
||||
array $args = []
|
||||
): static {
|
||||
if (!isset($this->config['sections'][$sectionId])) {
|
||||
public function addField(string $id, string $sectionId, string $type, string $label, array $args = []): static
|
||||
{
|
||||
if (! $this->config->has("sections.{$sectionId}")) {
|
||||
throw new InvalidArgumentException("Section '{$sectionId}' must be added before adding fields to it.");
|
||||
}
|
||||
if (!in_array($type, $this->fieldFactory->getSupportedTypes(), true)) {
|
||||
if (! in_array($type, $this->fieldFactory->getSupportedTypes(), true)) {
|
||||
throw new InvalidArgumentException("Field type '{$type}' is not supported.");
|
||||
}
|
||||
|
||||
$this->config['fields'][$id] = array_merge(
|
||||
[
|
||||
'id' => $id,
|
||||
'name' => $this->config['optionName'] . '[' . $id . ']',
|
||||
'section' => $sectionId,
|
||||
'type' => $type,
|
||||
'label' => $label,
|
||||
'description' => '',
|
||||
],
|
||||
$args
|
||||
);
|
||||
$fieldConfig = array_merge([
|
||||
'id' => $id,
|
||||
'name' => $this->getOptionName() . '[' . $id . ']',
|
||||
'section' => $sectionId,
|
||||
'type' => $type,
|
||||
'label' => $label,
|
||||
'description' => '',
|
||||
'labels' => $this->config->get('labels'),
|
||||
], $args);
|
||||
|
||||
$this->config->set("fields.{$id}", $fieldConfig);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -162,65 +226,61 @@ class Settings implements SettingsInterface
|
|||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->assetManager->setConfig($this->config);
|
||||
$this->assetManager->init();
|
||||
|
||||
add_action('admin_menu', [$this, 'registerPage']);
|
||||
add_action('admin_init', [$this, 'registerSettings']);
|
||||
$this->assetManager->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the settings page with the WordPress admin menu.
|
||||
*
|
||||
* @internal
|
||||
* @internal This method is a callback for the 'admin_menu' hook and should not be called directly.
|
||||
*/
|
||||
public function registerPage(): void
|
||||
{
|
||||
$renderer = new PageRenderer($this->config, $this->fieldFactory);
|
||||
|
||||
add_submenu_page(
|
||||
$this->config['parentSlug'],
|
||||
$this->config['pageTitle'],
|
||||
$this->config['menuTitle'],
|
||||
$this->config['capability'],
|
||||
$this->config['pageSlug'],
|
||||
[$renderer, 'renderPage']
|
||||
$this->config->get('parentSlug'),
|
||||
$this->config->get('pageTitle'),
|
||||
$this->config->get('menuTitle'),
|
||||
$this->config->get('capability'),
|
||||
$this->config->get('pageSlug'),
|
||||
[$this->pageRenderer, 'renderPage']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the settings, sections, and fields with the WordPress Settings API.
|
||||
*
|
||||
* @internal
|
||||
* @internal This method is a callback for the 'admin_init' hook and should not be called directly.
|
||||
*/
|
||||
public function registerSettings(): void
|
||||
{
|
||||
$this->determineAndSetActiveTab();
|
||||
$sanitizer = new Sanitizer($this->config, $this->fieldFactory);
|
||||
$renderer = new PageRenderer($this->config, $this->fieldFactory);
|
||||
|
||||
register_setting(
|
||||
$this->config['optionGroup'],
|
||||
$this->config['optionName'],
|
||||
$this->config->get('optionGroup'),
|
||||
$this->config->get('optionName'),
|
||||
['sanitize_callback' => [$sanitizer, 'sanitize']]
|
||||
);
|
||||
|
||||
foreach ($this->config['sections'] as $id => $section) {
|
||||
foreach ($this->config->get('sections', []) as $id => $section) {
|
||||
add_settings_section(
|
||||
$id,
|
||||
$section['title'],
|
||||
!empty($section['description'])
|
||||
! empty($section['description'])
|
||||
? fn() => print('<p class="section-description">' . wp_kses_post($section['description']) . '</p>')
|
||||
: '__return_null',
|
||||
$this->config['pageSlug']
|
||||
$this->config->get('pageSlug')
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->config['fields'] as $id => $field) {
|
||||
foreach ($this->config->get('fields', []) as $id => $field) {
|
||||
add_settings_field(
|
||||
$id,
|
||||
$field['label'],
|
||||
[$renderer, 'renderField'],
|
||||
$this->config['pageSlug'],
|
||||
[$this->pageRenderer, 'renderField'],
|
||||
$this->config->get('pageSlug'),
|
||||
$field['section'],
|
||||
['id' => $id, 'label_for' => $id]
|
||||
);
|
||||
|
@ -230,36 +290,97 @@ class Settings implements SettingsInterface
|
|||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getOptionName(): string
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->config['optionName'];
|
||||
}
|
||||
if (! isset($this->savedOptions)) {
|
||||
$optionsFromDb = get_option($this->getOptionName(), []);
|
||||
$this->savedOptions = is_array($optionsFromDb) ? $optionsFromDb : [];
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $this->savedOptions)) {
|
||||
return $this->savedOptions[$key];
|
||||
}
|
||||
|
||||
if (null === $default) {
|
||||
$default = $this->config->get("fields.{$key}.default");
|
||||
}
|
||||
|
||||
return $this->config->get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
public function getOptionName(): string
|
||||
{
|
||||
// First, check if the options have already been fetched for this request.
|
||||
if (!isset($this->options)) {
|
||||
// If not, fetch from the database once and cache the result.
|
||||
$this->options = get_option($this->getOptionName(), []);
|
||||
if (!is_array($this->options)) {
|
||||
$this->options = [];
|
||||
}
|
||||
return $this->config->get('optionName');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the active tab and stores it in the config object.
|
||||
*
|
||||
* @internal This is called late in the lifecycle to ensure all tabs have been registered.
|
||||
*/
|
||||
private function determineAndSetActiveTab(): void
|
||||
{
|
||||
if (empty($this->config->get('useTabs'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the value from the cached array, or the provided default.
|
||||
if (isset($this->options[$key])) {
|
||||
return $this->options[$key];
|
||||
$tabs = $this->config->get('tabs', []);
|
||||
if (empty($tabs)) {
|
||||
$this->config->set('useTabs', false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the key is not in the options, check for a configured default for that field.
|
||||
if (isset($this->config['fields'][$key]['default'])) {
|
||||
return $this->config['fields'][$key]['default'];
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$activeTab = sanitize_text_field(wp_unslash($_REQUEST['tab'] ?? ''));
|
||||
|
||||
if (empty($activeTab) || ! isset($tabs[$activeTab])) {
|
||||
$activeTab = (string)array_key_first($tabs);
|
||||
}
|
||||
|
||||
return $default;
|
||||
$this->config->set('activeTab', $activeTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides default definitions for external asset packages.
|
||||
*
|
||||
* @return array<string, mixed> The default asset package configurations.
|
||||
* @internal
|
||||
*/
|
||||
private function getDefaultAssetPackages(): array
|
||||
{
|
||||
return [
|
||||
'select2' => [
|
||||
'handle' => 'select2',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js',
|
||||
'deps' => ['jquery'],
|
||||
'version' => '4.0.13',
|
||||
'in_footer' => true
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css',
|
||||
'deps' => [],
|
||||
'version' => '4.0.13'
|
||||
],
|
||||
],
|
||||
'flatpickr' => [
|
||||
'handle' => 'flatpickr',
|
||||
'script' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js',
|
||||
'deps' => [],
|
||||
'version' => '4.6.13',
|
||||
'in_footer' => true
|
||||
],
|
||||
'style' => [
|
||||
'src' => 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css',
|
||||
'deps' => [],
|
||||
'version' => '4.6.13'
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue