Files

524 lines
15 KiB
PHP

<?php
/**
* WebToffee DS main class file.
*
* @since 1.0.0
* @package Wt_Ds
*/
namespace Wbte\Uimpexp\Ds;
if ( ! defined( 'WPINC' ) ) {
die;
}
use Wbte\Uimpexp\Ds\Classes\Wt_Ds_Template_Engine as Template_Engine;
/**
* To render the component.
*/
class Wbte_Ds {
/**
* Plugin specific prefix.
*
* @var string
*/
private $prefix = 'wbte_uimpexp_';
/**
* Plugin version.
*
* @var string
*/
private $version = '1.0.0';
/**
* To store template processing class object.
*
* @var null|object
*/
private $engine = null;
/**
* To save the component data loaded from the config JSON.
*
* @var array
*/
private $config = array();
/**
* Base path. To include files.
*
* @var string
*/
private $base_path = '';
/**
* Base URL. To link CSS, JS files.
*
* @var string
*/
private $base_url = '';
/**
* To store class object.
*
* @var null|object
*/
private static $instance = null;
/**
* Image relative path with respect to wp-content directory.
*
* @var string
*/
public $img_base_path = '';
/**
* Icon relative path with respect to wp-content directory.
*
* @var string
*/
public $icon_base_path = '';
/**
* Initiate the class.
*
* @param string $version Plugin version.
* @param string $prefix Plugin prefix(Optional).
*/
public function __construct( $version, $prefix = '' ) {
$this->base_path = plugin_dir_path( __FILE__ );
$this->base_url = plugin_dir_url( __FILE__ );
// Normalize paths for consistent handling across operating systems.
$wp_content_dir_normalized = wp_normalize_path( WP_CONTENT_DIR );
$base_path_normalized = wp_normalize_path( $this->base_path );
// Preparing icon base path with respect to wp content directory.
$this->icon_base_path = str_replace( $wp_content_dir_normalized, '', $base_path_normalized . 'icons/' );
// Preparing image base path with respect to wp content directory.
$this->img_base_path = str_replace( $wp_content_dir_normalized, '', $base_path_normalized . 'images/' );
include_once $this->base_path . 'classes/class-wt-ds-template-engine.php';
$this->prefix = $prefix ? $prefix : $this->prefix; // This is for plugins to use custom prefixes.
$this->version = $version; // Plugin version.
$this->load_config(); // Read the json file and store it as an array.
// Alter kses allowed.
add_filter( 'wp_kses_allowed_html', array( $this, 'kses_allowed_html' ), 10, 2 );
// Load the CSS and JS.
add_action( 'admin_enqueue_scripts', array( $this, 'load_styles_and_scripts' ), 10 );
$this->engine = new Template_Engine( $this->prefix ); // Initiate the template parsing class.
}
/**
* Get class instance.
*
* @param string $prefix Plugin prefix(Optional).
*/
public static function get_instance( $prefix = '' ) {
self::$instance = is_null( self::$instance ) ? new Wbte_Ds( $prefix ) : self::$instance;
return self::$instance;
}
/**
* Read the config JSON and store it as class variable.
*/
protected function load_config() {
$config_file = $this->base_path . 'wbte-ds-config.json';
if ( file_exists( $config_file ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$contents = file_get_contents( $config_file );
$this->config = $this->is_valid_json( $contents ) ? json_decode( $contents, true ) : array();
}
}
/**
* Enqueue CSS and JS.
*
* @access public
*/
public function load_styles_and_scripts() {
wp_enqueue_style( $this->prefix . 'ds_css', $this->base_url . 'css/style.css', array(), $this->version, 'all' );
wp_enqueue_script( $this->prefix . 'ds_js', $this->base_url . 'js/script.js', array( 'jquery' ), $this->version, false );
// Localize the script.
$params = array(
'icon_base_url' => esc_url( WP_CONTENT_URL . $this->icon_base_path ),
'img_base_url' => esc_url( WP_CONTENT_URL . $this->img_base_path ),
);
wp_localize_script( $this->prefix . 'ds_js', $this->prefix . 'ds_js_params', $params );
}
/**
* Checks the current JSON string is valid.
*
* @param string $json JSON string.
*/
protected function is_valid_json( $json ) {
if ( function_exists( 'json_validate' ) ) { // For PHP 8.3+.
return json_validate( $json );
} else {
json_decode( $json );
return ( json_last_error() === JSON_ERROR_NONE );
}
}
/**
* Fetch component template HTML from template file.
*
* @param string $slug Component slug.
*/
protected function fetch_template_html( $slug ) {
$file_path = $this->base_path . 'views/' . $slug . '.html';
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
return file_exists( $file_path ) ? file_get_contents( $file_path ) : '';
}
/**
* Prepare variation component slug.
*
* @param string $parent_slug Parent component slug.
* @param string $slug Component slug.
*/
public function prepare_variation_slug( $parent_slug, $slug ) {
return $parent_slug . '-' . $slug;
}
/**
* Get the component HTML.
*
* @param string $slug_str Component slug. If multiple slugs, separate by space. First one will be the parent and others are its attributes.
* @param array $args Array of arguments.
* @param object $engine Template engine object(Optional).
* @return string Component HTML or empty string.
*/
public function get_component( $slug_str, $args, $engine = null ) {
$html = '';
$slugs = array();
$slug_arr = explode( ' ', $slug_str ); // If multiple slugs. Assuming the first one will be the parent and others are its attributes.
$parent_slug = array_shift( $slug_arr );
$components_config = isset( $this->config['components'] ) && is_array( $this->config['components'] ) ? $this->config['components'] : array();
if ( ! empty( $components_config[ $parent_slug ] ) ) { // Component found.
$slugs[] = $parent_slug;
$parent_comp = $components_config[ $parent_slug ];
$html = $this->fetch_template_html( $parent_slug );
$attributes = ! empty( $parent_comp['attributes'] ) ? $parent_comp['attributes'] : array();
foreach ( $slug_arr as $slug ) {
// Prepare the attribute slug.
$attr_slug = $this->prepare_variation_slug( $parent_slug, $slug );
if ( ! empty( $attributes[ $attr_slug ] ) ) { // Attribute found.
$slugs[] = $attr_slug;
$attr_data = $attributes[ $attr_slug ];
$attr_html = $this->fetch_template_html( $attr_slug );
// Replace or append the HTML.
if ( ! empty( $attr_data['replace_html'] ) && ! empty( $attr_html ) ) {
$html = $attr_html; // Replace the HTML.
} else {
$html .= $attr_html; // Append the HTML.
}
}
}
// Generate HTML for pagination component.
if ( 'pagination' === $parent_slug ) {
$pagination_comp_html_args = isset( $args['values'] ) && is_array( $args['values'] ) ? $args['values'] : array();
$pagination_comp_html_args['html'] = $html;
$html = $this->prepare_pagination_component_html( $pagination_comp_html_args );
}
$args['html'] = $html;
$args['variations'] = $slugs;
$args['parent_obj'] = $this;
$args['icon_base'] = $this->icon_base_path;
$args['img_base'] = $this->img_base_path;
// Separate template engine object will be provided when component is rendered via component placeholder. This is to isolate the component from its parent component.
$html = is_null( $engine ) ? $this->engine->render( $args ) : $engine->render( $args );
return $html;
}
return '';
}
/**
* Get absolute URL of assets like icons, images.
* This will be useful when using DS assets directly.
*
* @param array $args Array of arguments.
* @return string Absolute URL of the asset. Empty string if the file not found.
*/
public function get_asset( $args ) {
$type = isset( $args['type'] ) ? $args['type'] : '';
$name = isset( $args['name'] ) ? $args['name'] : '';
if ( 'icon' === $type && file_exists( WP_CONTENT_DIR . $this->icon_base_path . $name . '.svg' ) ) {
return WP_CONTENT_URL . $this->icon_base_path . $name . '.svg';
} elseif ( 'image' === $type && file_exists( WP_CONTENT_DIR . $this->img_base_path . $name ) ) {
return WP_CONTENT_URL . $this->img_base_path . $name;
}
return '';
}
/**
* Get the component from component placeholders.
* This is using in inner component situations.
*
* @param array $args Array of arguments.
* @return string Component HTML or empty string.
*/
public function render_component_placeholder( $args ) {
$html = '';
$slugs = isset( $args['slug'] ) && is_array( $args['slug'] ) ? $args['slug'] : array();
if ( ! empty( $slugs ) ) {
$slug_str = implode( ' ', $slugs );
unset( $args['slug'] );
// This is to isolate the current component from other components.
$engine = new Template_Engine( $this->prefix );
$html = $this->get_component( $slug_str, $args, $engine );
unset( $engine );
}
return $html;
}
/**
* Prepare the HTML for pagination component.
*
* @param array $args The arguments for preparing the pagination component HTML.
* @return string The HTML for the pagination component.
*/
private function prepare_pagination_component_html( $args ) {
$total = $args['total'] ?? 0;
$limit = $args['limit'] ?? 0;
$mxnav = $args['max_nav'] ?? 5;
$crpage = $args['current_page'] ?? 1;
$html = $args['html'] ?? '';
$url = $args['url'] ?? '';
if ( $total <= 0 || $limit <= 0 ) {
return '';
}
if ( isset( $args['offset'] ) ) {
// Taking current page.
$offset = $args['offset'];
$crpage = floor( ( $offset + $limit ) / $limit );
$url_param = '{offset}';
} else {
$url_param = '{pagenum}';
}
$limit = $limit <= 0 ? 1 : $limit;
$ttpg = ceil( $total / $limit );
if ( $ttpg < $crpage ) {
return '';
}
// Pagination calculations.
$mxnav = $ttpg < $mxnav ? $ttpg : $mxnav;
$mxnav_mid = floor( $mxnav / 2 );
$pgstart = $mxnav_mid >= $crpage ? 1 : $crpage - $mxnav_mid;
$mxnav_mid += $mxnav_mid >= $crpage ? ( $mxnav_mid - $crpage ) : 0; // Adjusting the other half with the first half balance.
$pgend = $crpage + $mxnav_mid;
if ( $pgend > $ttpg ) {
$pgend = $ttpg;
}
$find_replace = array();
// Pagination attributes.
$pagination_attr = ' data-offset="{offset}" data-pagenum="{pagenum}"' . ( $url ? ' href="' . esc_url( $url ) . $url_param . '"' : '' );
// Previous button.
$prev_offset_attr = '';
if ( $crpage > 1 ) {
$offset = ( ( $crpage - 2 ) * $limit );
$prev_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, ( $crpage - 1 ) ), $pagination_attr );
}
$find_replace['{{prev_button}}'] = '<a class="' . ( $crpage <= 1 ? 'disabled' : 'pagenav' ) . '"' . $prev_offset_attr . '>{{wbte-ds-icon-left-arrow-2}}</a>';
// First page.
if ( 1 < $pgstart ) {
$page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( 0, 1 ), $pagination_attr );
$find_replace['{{first_page}}'] = '<a class="pagenav" ' . $page_offset_attr . '>1</a>';
}
// First dots.
if ( 2 < $pgstart ) {
$find_replace['{{first_dots}}'] = '<a class="dots">...</a>';
}
// Menu numbers.
$find_replace['{{numbers}}'] = '';
for ( $i = $pgstart; $i <= $pgend; $i++ ) {
$i = (int) $i;
$page_offset = '';
$page_offset_attr = '';
$offset = ( $i * $limit ) - $limit;
if ( $i !== $crpage ) {
$page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, $i ), $pagination_attr );
}
$find_replace['{{numbers}}'] .= '<a class="' . ( $i === $crpage ? 'current' : 'pagenav' ) . '" ' . $page_offset_attr . '>' . $i . '</a>';
}
// Last dots.
if ( $pgend <= ( $ttpg - 2 ) ) {
$find_replace['{{last_dots}}'] = '<a class="dots">...</a>';
}
// Last page.
if ( $pgend < $ttpg ) {
$page_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( ( ( $ttpg - 1 ) * $limit ), $ttpg ), $pagination_attr );
$find_replace['{{last_page}}'] = '<a class="pagenav" ' . $page_offset_attr . '>' . $ttpg . '</a>';
}
// Next button.
$next_offset_attr = '';
if ( $crpage < $ttpg ) {
$offset = ( $crpage * $limit );
$next_offset_attr = str_replace( array( '{offset}', '{pagenum}' ), array( $offset, ( $crpage + 1 ) ), $pagination_attr );
}
$find_replace['{{next_button}}'] = '<a class="' . ( $crpage < $ttpg ? 'pagenav' : 'disabled' ) . '"' . $next_offset_attr . '>{{wbte-ds-icon-right-arrow-3}}</a>';
return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $html );
}
/**
* Render the HTML.
*
* @param array $args Array of arguments.
* @return string Rendered HTML.
*/
public function render_html( $args ) {
$args = is_array( $args ) ? $args : array();
$args['html'] = $args['html'] ?? '';
$args['variations'] = $args['variations'] ?? array();
$args['parent_obj'] = $this;
$args['icon_base'] = $this->icon_base_path;
$args['img_base'] = $this->img_base_path;
return $this->engine->render( $args );
}
/**
* Alter kses allowed HTML.
*
* @param array $allowedposttags Allowed tags.
* @param string $context Context.
* @return array Allowed tags.
*/
public function kses_allowed_html( $allowedposttags, $context ) {
if ( 'post' === $context ) {
// Define SVG tags and their safer allowed attributes.
$svg_tags = array(
'svg' => array(
'xmlns' => true,
'width' => true,
'height' => true,
'viewBox' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'preserveAspectRatio' => true,
),
'g' => array(
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'rect' => array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'line' => array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'stroke' => true,
'stroke-width' => true,
),
'polygon' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'polyline' => array(
'points' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
'text' => array(
'x' => true,
'y' => true,
'dx' => true,
'dy' => true,
'text-anchor' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
),
);
$allowedposttags = array_merge( $allowedposttags, $svg_tags );
}
return $allowedposttags;
}
}