<?php
namespace FindStr;
use Handlebars\Handlebars;
use phpDocumentor\Reflection\File;
class Template {
private static array $instance = array();
public string $group = 'default';
public Handlebars $handlebars;
public string $template_dir = FINDSTR_PLUGIN_PATH . 'templates/';
public string $template_theme_dir = 'findstr/';
public bool $enable_search_overlay = false;
public array $results = array();
public array $translations = array();
public array $handlebars_helpers = array();
/**
* @param string $group
*
* @return Template[]
*/
public static function get_instance( string $group = 'default' ) {
if ( empty( self::$instance[ $group ] ) ) {
self::$instance[ $group ] = new Template( $group );
}
return self::$instance[ $group ];
}
private function __construct( $group ) {
$this->group = $group;
//By default, we set the post types translations
add_action( 'wp', array( $this, 'set_post_types_translations' ) );
/**
* Filter the template theme directory path
*
* @hook findstr_template_theme_dir
*
* @param {string} $template_theme_dir
*
* @return {string} $template_theme_dir The template theme directory path
*/
$this->template_theme_dir = apply_filters( 'findstr_template_theme_dir', $this->template_theme_dir );
$partials_loader = new HandlebarsTemplateLoader(
array(
trailingslashit( get_stylesheet_directory() ) . $this->template_theme_dir,
trailingslashit( get_template_directory() ) . $this->template_theme_dir,
$this->template_dir,
)
);
$this->handlebars = new Handlebars(
array(
'loader' => $partials_loader,
'partials_loader' => $partials_loader,
'enableDataVariables' => true,
)
);
$this->handlebars_register_helpers();
if ( ! empty( $group ) ) {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'wp_footer', array( $this, 'display_search_overlay' ) );
}
add_filter( 'findstr_search_results', array( $this, 'search_results_permalinks' ) );
}
public function enqueue_scripts() {
$settings = ( new \FindStr\ServerSettings() )->get();
if ( empty( $settings->serverUrl ) || empty( $settings->indexUID ) || empty( $settings->publicKey ) ) {
return;
}
$assets = new \FindStr\RegisterAssets( 'front/findstr-search', 'front/findstr-search', array( 'moment' ) );
$assets->add_data(
'findstr',
/**
* Filter the front script data.
* This filter is used to pass data to the front script, such as the configuration, translations, etc.
* This data will be available in the front script in the global variable `findstr`
*
* @hook findstr_front_script_data
*
* @param {array} $data
*
* @return {array} $data
*/
apply_filters(
'findstr_front_script_data',
array(
'config' => array(
'host' => $settings->serverUrl,
'index' => $settings->indexUID,
'publicKey' => $settings->publicKey,
'defaultQuery' => wp_json_encode( Search::get_instance( 'default' )->get_default_query_args() ),
'dateFormats' => $this->get_date_formats(),
),
'translations' => $this->get_translations(),
'currentLanguage' => Helpers::get_language_code(),
)
)
);
$assets->set_translations( 'findstr', FINDSTR_PLUGIN_PATH . '/languages' );
$assets->enqueue();
$analytics = new RegisterAssets( 'front/analytics', 'front/analytics', array( 'front/findstr-search' ) );
$analytics->enqueue();
}
public static function get_date_formats( $format = null ) {
$date_formats = array(
/**
* Filter the date formats.
*
* This filter is used to modify the date formats used in the front script, it's useful to adapt the date format to the website's language.
*
* "format" can be 'short', 'medium', 'long', 'full'
*
*
* @hook findstr_date_format_{format}
*
* @param {array} $date_formats
*
* @return {array} $date_formats
*/
'short' => apply_filters( 'findstr_date_format_short', _x( 'Y-m-d', 'Frontend date format', 'findstr' ) ),
'medium' => apply_filters( 'findstr_date_format_medium', _x( 'Y/m/d H:i:s', 'Frontend date format', 'findstr' ) ),
'long' => apply_filters( 'findstr_date_format_long', _x( 'F j, Y', 'Frontend date format', 'findstr' ) ),
'full' => apply_filters( 'findstr_date_format_full', _x( 'D j F Y, H:i:s', 'Frontend date format', 'findstr' ) ),
);
if ( ! empty( $format ) ) {
return( ! empty( $date_formats[ $format ] ) ? $date_formats[ $format ] : current( $date_formats ) );
}
return $date_formats;
}
/**
* Set a translation for a key
*
* @param $key
* @param $value
* @param $context
*
* @return void
*/
public function set_translation( $key, $value, $context = 'findstr' ) {
$this->translations[ $context ][ $key ] = $value;
}
public function get_translations(): array {
/**
* Filter the translations.
* This filter is used to modify the translations used in the front script.
*
* @hook findstr_translations
*
* @param {array} $translations
*
* @return {array} $translations
*/
return (array) apply_filters( 'findstr_translations', $this->translations );
}
public function set_post_types_translations() {
$post_types = get_post_types( array( 'public' => true ), 'objects' );
foreach ( $post_types as $post_type ) {
$this->set_translation( $post_type->name, $post_type->label, 'post_type' );
}
}
/**
* This function is used to parse the arguments passed to the handlebars helper.
*
* @param $args
*
* @return array
*/
private function handlebars_parse_args( $args ) {
return str_getcsv( $args, ' ', '\'' );
}
/**
* Register handlebars helpers.
* @return void
*/
private function handlebars_register_helpers() {
$this->handlebars_add_helper(
'dateI18n',
function( $template, $context, $args, $source ) {
//enqueue the wp date script for the dateI18n helper in handlebars JS
wp_enqueue_script( 'wp-date' );
$format = $this->handlebars_parse_args( $args );
$tpl = $template->getEngine()->loadString( $source );
$date = $tpl->render( $context );
$date_format = self::get_date_formats( current( $format ) );
return date_i18n( $date_format, strtotime( $date ) );
}
);
$this->handlebars_add_helper(
'timeSince',
function ( $template, $context, $args, $source ) {
$tpl = $template->getEngine()->loadString( $source );
$date = $tpl->render( $context );
return human_time_diff( strtotime( $date ) );
}
);
$this->handlebars_add_helper(
'log',
function ( $template, $context, $args, $source ) {
if ( true !== WP_DEBUG ) {
return '';
}
$args = explode( ' ', $args );
$log = '';
foreach ( $args as $arg ) {
if ( ! empty( $context->get( $arg ) ) ) {
$log .= wp_json_encode( $context->get( $arg ) );
} else {
$log .= $arg;
}
$log .= ', ';//separate logs by comma for console.log
}
return '<script type="application/javascript"> console.log(' . $log . ');</script>'; //phpcs:ignore: WordPress.PHP.DevelopmentFunctions.error_log_var_export
}
);
$this->handlebars_add_helper(
'compare',
function( $template, $context, $args, $source ) {
$args = $this->handlebars_parse_args( $args );
if ( count( $args ) !== 3 ) {
return '';
}
$lvalue = $context->get( $args[0] );
$operator = str_replace( '"', '', $args[1] );
$rvalue = '' === $context->get( $args[2] ) ? $context->get( $args[2] ) : $args[2];
switch ( $operator ) {
case '==':
// phpcs:ignore
$result = $lvalue == $rvalue;
break;
case '===':
$result = $lvalue === $rvalue;
break;
case '!=':
// phpcs:ignore
$result = $lvalue != $rvalue;
break;
case '!==':
$result = $lvalue !== $rvalue;
break;
case '<':
$result = $lvalue < $rvalue;
break;
case '>':
$result = $lvalue > $rvalue;
break;
case '<=':
$result = $lvalue <= $rvalue;
break;
case '>=':
$result = $lvalue >= $rvalue;
break;
case '&&':
$result = $lvalue && $rvalue;
break;
case '||':
$result = $lvalue || $rvalue;
break;
case 'typeof':
$result = gettype( $lvalue ) === $rvalue;
break;
default:
return ''; //unsupported operator
}
if ( $result ) {
$tpl = $template->getEngine()->loadString( $source );
return $tpl->render( $context );
}
return '';
}
);
}
public function handlebars_add_helper( $name, $callback ) {
$this->handlebars_helpers[] = $name;
$this->handlebars->addHelper( $name, $callback );
}
public function locate_template( $template_name ) {
$template = locate_template(
$this->template_theme_dir . $template_name . '.php'
);
if ( ! empty( $template ) ) {
$template_name = $template_name . '.php';
} else {
$template = locate_template(
$this->template_theme_dir . $template_name
);
}
// Get default template, .php first
if ( ! $template ) {
if ( is_file( FINDSTR_PLUGIN_PATH . 'templates/' . $template_name . '.php' ) ) {
$template = FINDSTR_PLUGIN_PATH . 'templates/' . $template_name . '.php';
$template_name = $template . '.php';
} else {
$template = FINDSTR_PLUGIN_PATH . 'templates/' . $template_name;
}
}
/**
* Filter the located template.
* Use this filter to modify the template path.
*
* @hook findstr_locate_template
*
* @param {string} $template
* @param {string} $template_name
*
* @return {string} $template
*/
return apply_filters( 'findstr_locate_template', $template, $template_name );
}
public function get_handlebars_template( $template_name ) {
return $this->handlebars->getPartialsLoader()->load( $template_name );
}
public function get_source( $template_file ) {
ob_start();
include $this->locate_template( $template_file );
return ob_get_clean();
}
public function the_template_source( $template_name ) {
echo $this->get_source( $template_name ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function render_template( $string, $data ) {
$template = $this->handlebars->loadString( $string );
return $template->render( $data );
}
public function the_render( $template_name, $data ) {
$source = $this->get_source( $template_name );
echo $this->render_template( $source, $data ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function display_results( $default_results = array() ) {
if ( false === $default_results ) {
$default_results = array();
}
if ( true === $default_results ) {
$default_results = $this->get_results();
}
$default_results['group'] = $this->group;
$default_results['query'] = Search::get_instance( $this->group )->get_query();
$this->get_template( 'results-container', $default_results );
}
public function display_search_overlay() {
global $overlay_already_loaded;
/**
* Filter to enable or disable the search overlay.
* This filter is used to enable or disable the search overlay.
*
* In the front-end, the search overlay is enabled only if used in a block or in menu item.
* If you want to enable the search overlay in the front-end with custom code, you must use this filter to enable it.
*
* Minimal markup example:
* ```php
* <a href="#" class="findstrOpenModal">Open Modal</a>
* ```
* OR
* ```php
* <button class="findstrOpenModal">Open Modal</button>
* ```
*
*
*
* @hook findstr_enable_filter_overlay
*
* @param {bool} $enable_search_overlay
*
* @return {bool} $enable_search_overlay
*
*/
$this->enable_search_overlay = apply_filters( 'findstr_enable_filter_overlay', $this->enable_search_overlay );
if ( $this->enable_search_overlay || defined( 'FINDSTR_SEARCH_MENU_ITEM' ) ) {
if ( ! $overlay_already_loaded ) {
$this->get_template( 'main-search-overlay' );
( new RegisterAssets( 'front/search-overlay', 'front/search-overlay' ) )->enqueue();
$overlay_already_loaded = true;
}
}
}
/**
* Get Template
*
* @param $template_name
* @param $args
* @param $template_path
* @param $default_path
*
* @return void
*/
public function get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
$template_file = $this->locate_template( $template_path . $template_name . '.php' );
if ( ! file_exists( $template_file ) ) {
$template_name = Helpers::camel2dash( $template_name );
$template_file = $this->locate_template( $default_path . $template_name . '.php' );
}
if ( file_exists( $template_file ) ) {
/**
* Filter the template arguments.
* Before the template is loaded arguments can be modified.
*
* @hook findstr_template_args
*
* @param {array} $args
* @param {string} $template_name
*
* @return {array} $args
*/
$args = apply_filters( 'findstr_template_args', $args, $template_name );
/**
* Fires before the template part is loaded.
*
* @hook findstr_before_template_part
*
* @param {string} $template_name Name of the template part.
* @param {string} $template_path Path to the template part.
* @param {string} $template_file Path to the template file.
* @param {array} $args Arguments passed to the template.
*
*/
do_action( 'findstr_before_template_part', $template_name, $template_path, $template_file, $args );
include $template_file;
/**
* Fires after the template part is loaded.
*
* @hook findstr_after_template_part
*
* @param {string} $template_name Name of the template part.
* @param {string} $template_path Path to the template part.
* @param {string} $template_file Path to the template file.
* @param {array} $args Arguments passed to the template.
*
*/
do_action( 'findstr_after_template_part', $template_name, $template_path, $template_file, $args );
}
}
/**
* Display a field.
*
* @param string $slug The field slug.
* @param array $args The field arguments, data will be available in the template.
* @return false|void
*/
public function display_field( string $slug, array $args = array() ) {
$field = ( new \FindStr\Fields() )->get_field( $slug );
if ( empty( $field ) ) {
return false;
}
$args['fieldId'] = $field->id;
$args['fieldGroup'] = $this->group;
$args['field'] = $field;
if ( ! empty( $this->get_field_queried_search( $field ) ) ) {
$args['value'] = $this->get_field_queried_search( $field );
}
/**
* Filter the field arguments.
* Before the field is displayed arguments can be modified.
*
* @hook findstr_field_args
*
* @param {array} $args
* @param {string} $slug
* @param {string} $group
*
* @return {array} $args
*/
$args = apply_filters( 'findstr_field_args', $args, $slug, $this->group );
if ( ! empty( $args['field'] ) ) {
$field = $args['field'];
if ( ! empty( $field->type ) ) {
( new RegisterAssets( 'front/fields-' . $field->type ) )->enqueue();
}
}
$this->get_template( 'fields/base-field', $args );
}
/**
* @Deprecated use display_field instead
*
* @param string $slug
* @param array $args
* @return void
*/
public function get_field( string $slug, array $args = array() ) {
$this->display_field( $slug, $args );
}
public function get_search_button( $args ): string {
$args = wp_parse_args(
$args,
array(
'classes' => array( '' ),
)
);
/**
* Filter the search button arguments
*
* @hook findstr_search_button_args
*
* @param {array} $args
*
* @return {array} $args
*/
$args = apply_filters( 'findstr_search_button_args', $args );
$classes = implode( ' ', $args['classes'] );
$search_button_html = '<button id="findstr-search-trigger" class="' . $classes . '">
<i class="icon icon-recherche" aria-hidden="true"></i>
<span class="sr-only">' . esc_html_x( 'Open the search window.', 'button aria label', 'findstr' ) . '</span>
</button>';
/**
* Filter the search button html
*
* @hook findstr_search_button_html
*
* @param {string} $search_button_html
* @param {array} $args
*
* @return {string} $search_button_html
*/
return apply_filters( 'findstr_search_button_html', $search_button_html, $args );
}
/**
* Use this function to set the default query for a group
*
* @param $query
*
* @return array
*/
public function update_query( $query ): array {
return Search::get_instance( $this->group )->update_query( $query );
}
public function set_query_arg( $key, $value ) {
Search::get_instance( $this->group )->set_query_arg( $key, $value );
}
public function add_filter( $key, $value, string $compare = '=' ) {
Search::get_instance( $this->group )->add_filter( $key, $value, $compare );
}
/**
* Use this function to get the results for a query.
* Mainly used for php rendering.
*
*
* @param bool $force_query
*
* @return array
*/
public function get_results( bool $force_query = false ): array {
if ( defined( 'DOING_FINDSTR_INDEX' ) && true === DOING_FINDSTR_INDEX ) {
return array();
}
if ( empty( $this->results ) || $force_query ) {
try {
$search = Search::get_instance( $this->group );
$search_results = $search->fetch_results()->toArray();
$search_results['translations'] = $this->get_translations();
$search_results['hits'] = array_map(
function ( $hit ) {
$hit['post_type_label'] = ! empty( get_post_type_object( $hit['post_type'] ) ) ? get_post_type_object( $hit['post_type'] )->label : $hit['post_type'];
return $hit;
},
$search_results['hits']
);
$language_code = Helpers::get_language_code();
$results_count = $search_results['facetDistribution']['language'][ $language_code ] ?? $search_results['totalHits'];
$search_results['totalHits'] = $results_count;
$this->results = $search_results;
} catch ( \Exception $e ) {
new Log( 'Get results error', 'error', array( 'message' => $e->getMessage() ) );
return array();
}
}
/**
* Filter the results for a group
*
* @hook findstr_search_results
*
* @param {array} $results
* @param {string} $group
*
* @return {array} $results
*/
return apply_filters( 'findstr_search_results', $this->results, $this->group );
}
/**
* Get queried search for group
*
* @param string $type
*
* @return array|string
*/
public function get_queried_search( string $type = 'filter' ) {
$queried_search = (array) get_query_var( 'findstr' );
//unsplash all values in the array (with key "value")
$queried_search = wp_unslash( $queried_search );
// If we have a search query for this group and type.
if ( ! empty( $queried_search ) &&
! empty( $queried_search['group'] ) &&
$queried_search['group'] === $this->group &&
! empty( $queried_search[ $type ] ) ) {
return $queried_search[ $type ];
}
return array();
}
public function get_query() {
return Search::get_instance( $this->group )->get_query();
}
public function get_field_queried_search( $field ) {
//map type to query var.
switch ( $field->type ) {
case 'search':
$type = 'q';
break;
case 'sort':
$type = 'sort';
break;
case 'pagination':
case 'loadMore':
$type = 'page';
break;
default:
$type = 'filter';
break;
}
$queried = $this->get_queried_search( $type );
if ( empty( $queried ) ) {
return array();
}
if ( ! empty( $queried[ $field->id ] ) ) {
return $queried[ $field->id ];
}
if ( ! empty( $queried[ $field->source_name ] ) ) {
return $queried[ $field->source_name ];
}
return $queried;
}
public function build_url( $args = array() ) {
$url = '?' . http_build_query( array( 'findstr' => $args ) );
return urldecode( $url );
}
/**
* Create a pagination array.
* Mainly used for php rendering.
* If a modification is needed here, you also need to modify the javascript pagination function.
* front/findstr-search/src/fields/pagination.js
*
*
* @return array
*/
public function pagination( $url ): array {
$results = $this->results;
if ( ! isset( $results['page'] ) ) {
return array();
}
$current = $results['page'];
$last = $results['totalPages'];
$delta = 2;
$left = $current - $delta;
$right = $current + $delta + 1;
$range = array();
$range_with_dots = array();
$l = null;
/* translators: %s is current page */
$current_label = _x( 'Page %d (current)', 'pagination label', 'findstr' );
/* translators: %s is current page */
$page_label = _x( 'Page %d', 'pagination label', 'findstr' );
for ( $i = 1; $i <= $last; $i++ ) {
if ( 1 === $i || $i === $last || ( $i >= $left && $i < $right ) ) {
$label = $i === $current ? $current_label : $page_label;
$url['page'] = $i;
$range[] = array(
'page' => $i,
'current' => $i === $current,
'label' => sprintf( $label, $i ),
'url' => $this->build_url( $url ),
);
}
}
foreach ( $range as $item ) {
$page = $item['page'];
$current = $item['current'];
$label = $item['label'];
$url['page'] = $page;
if ( $l ) {
if ( 2 === $page - $l ) {
$range_with_dots[] = array(
'page' => $l + 1,
'current' => $current,
'label' => $label,
'url' => $this->build_url( $url ),
);
} elseif ( 1 !== $page - $l ) {
$range_with_dots[] = array(
'page' => '...',
'current' => $current,
'isEllipsis' => true,
'label' => _x( 'Ellipsis', 'pagination label', 'findstr' ),
);
}
}
$range_with_dots[] = array(
'page' => $page,
'current' => $current,
'label' => $label,
'url' => $this->build_url( $url ),
);
$l = $page;
}
return array(
'pages' => $range_with_dots,
'isFirstPage' => 1 === $results['page'],
'isLastPage' => $results['page'] === $results['totalPages'],
'nextLabel' => _x( 'Next page', 'pagination label', 'findstr' ),
'previousLabel' => _x( 'Previous page', 'pagination label', 'findstr' ),
);
}
public function search_results_permalinks( $results ) {
$results['hits'] = array_map(
function ( $hit ) {
if ( is_array( $hit['language'] ) && ! empty( $hit['permalinks'] ) ) {
$hit['permalink'] = $hit['permalinks'][ Helpers::get_language_code() ];
}
return $hit;
},
$results['hits']
);
return $results;
}
}