Migrate interactivity stock filter to new store API, add improvements and bugfixes (https://github.com/woocommerce/woocommerce-blocks/pull/11827)
This commit is contained in:
parent
590263543f
commit
1cd4df5b19
|
@ -1,10 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
store as interactivityStore,
|
||||
navigate,
|
||||
} from '@woocommerce/interactivity';
|
||||
import { store, navigate, getContext } from '@woocommerce/interactivity';
|
||||
import { DropdownContext } from '@woocommerce/interactivity-components/dropdown';
|
||||
import { HTMLElementEvent } from '@woocommerce/types';
|
||||
|
||||
|
@ -21,59 +18,40 @@ const getUrl = ( activeFilters: string ) => {
|
|||
return url.href;
|
||||
};
|
||||
|
||||
type StockFilterState = {
|
||||
filters: {
|
||||
stockStatus: string;
|
||||
activeFilters: string;
|
||||
showDropdown: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
state: StockFilterState;
|
||||
event: HTMLElementEvent< HTMLInputElement >;
|
||||
};
|
||||
|
||||
interactivityStore( {
|
||||
state: {
|
||||
filters: {
|
||||
stockStatus: '',
|
||||
},
|
||||
},
|
||||
store( 'woocommerce/collection-stock-filter', {
|
||||
actions: {
|
||||
filters: {
|
||||
navigate: ( { context }: { context: DropdownContext } ) => {
|
||||
if ( context.woocommerceDropdown.selectedItem.value ) {
|
||||
navigate(
|
||||
getUrl( context.woocommerceDropdown.selectedItem.value )
|
||||
);
|
||||
// "on select" handler passed to the dropdown component.
|
||||
navigate: () => {
|
||||
const context = getContext< DropdownContext >(
|
||||
'woocommerce/interactivity-dropdown'
|
||||
);
|
||||
|
||||
navigate( getUrl( context.selectedItem.value || '' ) );
|
||||
},
|
||||
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
||||
// get the active filters from the url:
|
||||
const url = new URL( window.location.href );
|
||||
const currentFilters =
|
||||
url.searchParams.get( 'filter_stock_status' ) || '';
|
||||
|
||||
// split out the active filters into an array.
|
||||
const filtersArr =
|
||||
currentFilters === '' ? [] : currentFilters.split( ',' );
|
||||
|
||||
// if checked and not already in activeFilters, add to activeFilters
|
||||
// if not checked and in activeFilters, remove from activeFilters.
|
||||
if ( event.target.checked ) {
|
||||
if ( ! currentFilters.includes( event.target.value ) ) {
|
||||
filtersArr.push( event.target.value );
|
||||
}
|
||||
},
|
||||
updateProducts: ( { event }: ActionProps ) => {
|
||||
// get the active filters from the url:
|
||||
const url = new URL( window.location.href );
|
||||
const currentFilters =
|
||||
url.searchParams.get( 'filter_stock_status' ) || '';
|
||||
|
||||
// split out the active filters into an array.
|
||||
const filtersArr =
|
||||
currentFilters === '' ? [] : currentFilters.split( ',' );
|
||||
|
||||
// if checked and not already in activeFilters, add to activeFilters
|
||||
// if not checked and in activeFilters, remove from activeFilters.
|
||||
if ( event.target.checked ) {
|
||||
if ( ! currentFilters.includes( event.target.value ) ) {
|
||||
filtersArr.push( event.target.value );
|
||||
}
|
||||
} else {
|
||||
const index = filtersArr.indexOf( event.target.value );
|
||||
if ( index > -1 ) {
|
||||
filtersArr.splice( index, 1 );
|
||||
}
|
||||
} else {
|
||||
const index = filtersArr.indexOf( event.target.value );
|
||||
if ( index > -1 ) {
|
||||
filtersArr.splice( index, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
navigate( getUrl( filtersArr.join( ',' ) ) );
|
||||
},
|
||||
navigate( getUrl( filtersArr.join( ',' ) ) );
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -100,9 +100,18 @@ export default () => {
|
|||
|
||||
// data-wc-on--[event]
|
||||
directive( 'on', ( { directives: { on }, element, evaluate } ) => {
|
||||
const events = new Map();
|
||||
on.forEach( ( entry ) => {
|
||||
element.props[ `on${ entry.suffix }` ] = ( event ) => {
|
||||
evaluate( entry, event );
|
||||
const event = entry.suffix.split( '--' )[ 0 ];
|
||||
if ( ! events.has( event ) ) events.set( event, new Set() );
|
||||
events.get( event ).add( entry );
|
||||
} );
|
||||
|
||||
events.forEach( ( entries, event ) => {
|
||||
element.props[ `on${ event }` ] = ( event ) => {
|
||||
entries.forEach( ( entry ) => {
|
||||
evaluate( entry, event );
|
||||
} );
|
||||
};
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,95 +1,93 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { store as interactivityStore } from '@woocommerce/interactivity';
|
||||
import { getContext, store } from '@woocommerce/interactivity';
|
||||
|
||||
export type DropdownContext = {
|
||||
woocommerceDropdown: {
|
||||
currentItem: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
selectedItem: {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
};
|
||||
hoveredItem: {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
};
|
||||
isOpen: boolean;
|
||||
currentItem: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
selectedItem: {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
};
|
||||
hoveredItem: {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
};
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type Store = {
|
||||
context: DropdownContext;
|
||||
selectors: unknown;
|
||||
ref: HTMLElement;
|
||||
};
|
||||
|
||||
interactivityStore( {
|
||||
store( 'woocommerce/interactivity-dropdown', {
|
||||
state: {},
|
||||
selectors: {
|
||||
woocommerceDropdown: {
|
||||
placeholderText: ( { context }: { context: DropdownContext } ) => {
|
||||
const {
|
||||
woocommerceDropdown: { selectedItem },
|
||||
} = context;
|
||||
placeholderText: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
const { selectedItem } = context;
|
||||
|
||||
return selectedItem.label || 'Select an option';
|
||||
},
|
||||
isSelected: ( { context }: { context: DropdownContext } ) => {
|
||||
const {
|
||||
woocommerceDropdown: {
|
||||
currentItem: { value },
|
||||
},
|
||||
} = context;
|
||||
return selectedItem.label || 'Select an option';
|
||||
},
|
||||
isSelected: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
return (
|
||||
context.woocommerceDropdown.selectedItem.value === value ||
|
||||
context.woocommerceDropdown.hoveredItem.value === value
|
||||
);
|
||||
},
|
||||
const {
|
||||
currentItem: { value },
|
||||
} = context;
|
||||
|
||||
return (
|
||||
context.selectedItem.value === value ||
|
||||
context.hoveredItem.value === value
|
||||
);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
woocommerceDropdown: {
|
||||
toggleIsOpen: ( store: Store ) => {
|
||||
const {
|
||||
context: { woocommerceDropdown },
|
||||
} = store;
|
||||
toggleIsOpen: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
woocommerceDropdown.isOpen = ! woocommerceDropdown.isOpen;
|
||||
},
|
||||
selectDropdownItem: ( {
|
||||
context,
|
||||
}: {
|
||||
context: DropdownContext;
|
||||
} ) => {
|
||||
const {
|
||||
woocommerceDropdown: {
|
||||
currentItem: { label, value },
|
||||
},
|
||||
} = context;
|
||||
context.isOpen = ! context.isOpen;
|
||||
},
|
||||
selectDropdownItem: ( event: MouseEvent ) => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
context.woocommerceDropdown.selectedItem = { label, value };
|
||||
context.woocommerceDropdown.isOpen = false;
|
||||
},
|
||||
addHoverClass: ( { context }: { context: DropdownContext } ) => {
|
||||
const {
|
||||
woocommerceDropdown: {
|
||||
currentItem: { label, value },
|
||||
},
|
||||
} = context;
|
||||
const {
|
||||
currentItem: { label, value },
|
||||
} = context;
|
||||
|
||||
context.woocommerceDropdown.hoveredItem = { label, value };
|
||||
},
|
||||
removeHoverClass: ( { context }: { context: DropdownContext } ) => {
|
||||
context.woocommerceDropdown.hoveredItem = {
|
||||
const { selectedItem } = context;
|
||||
|
||||
if (
|
||||
selectedItem.value === value &&
|
||||
selectedItem.label === label
|
||||
) {
|
||||
context.selectedItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
} else {
|
||||
context.selectedItem = { label, value };
|
||||
}
|
||||
|
||||
context.isOpen = false;
|
||||
|
||||
event.stopPropagation();
|
||||
},
|
||||
addHoverClass: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
const {
|
||||
currentItem: { label, value },
|
||||
} = context;
|
||||
|
||||
context.hoveredItem = { label, value };
|
||||
},
|
||||
removeHoverClass: () => {
|
||||
const context = getContext< DropdownContext >();
|
||||
|
||||
context.hoveredItem = {
|
||||
label: null,
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -47,17 +47,6 @@ final class CollectionStockFilter extends AbstractBlock {
|
|||
$stock_status_counts = $block->context['collectionData']['stock_status_counts'] ?? [];
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
|
||||
wc_store(
|
||||
array(
|
||||
'state' => array(
|
||||
'filters' => array(
|
||||
'stockStatus' => $stock_status_counts,
|
||||
'activeFilters' => '',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-stock-filter__controls">%2$s</div>
|
||||
|
@ -77,6 +66,7 @@ final class CollectionStockFilter extends AbstractBlock {
|
|||
*/
|
||||
private function get_stock_filter_html( $stock_counts, $attributes ) {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
$stock_statuses = wc_get_product_stock_status_options();
|
||||
|
||||
// check the url params to select initial item on page load.
|
||||
|
@ -84,9 +74,10 @@ final class CollectionStockFilter extends AbstractBlock {
|
|||
$selected_stock_status = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$list_items = array_map(
|
||||
function( $item ) use ( $stock_statuses ) {
|
||||
function( $item ) use ( $stock_statuses, $show_counts ) {
|
||||
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
|
||||
return array(
|
||||
'label' => $stock_statuses[ $item['status'] ],
|
||||
'label' => $label,
|
||||
'value' => $item['status'],
|
||||
);
|
||||
},
|
||||
|
@ -108,63 +99,70 @@ final class CollectionStockFilter extends AbstractBlock {
|
|||
'value' => null,
|
||||
);
|
||||
|
||||
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-stock-filter' ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<?php if ( 'list' === $display_style ) : ?>
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $stock_counts as $stock_count ) { ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $stock_count['status'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.filters.updateProducts"
|
||||
value="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
<?php checked( strpos( $selected_stock_status, $stock_count['status'] ) !== false, 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php echo esc_html( $stock_statuses[ $stock_count['status'] ] ); ?>
|
||||
<?php
|
||||
// translators: %s: number of products.
|
||||
$screen_reader_text = sprintf( _n( '%s product', '%s products', $stock_count['count'], 'woo-gutenberg-products-block' ), number_format_i18n( $stock_count['count'] ) );
|
||||
?>
|
||||
<span class="wc-filter-element-label-list-count">
|
||||
<span aria-hidden="true">
|
||||
<?php echo esc_html( $stock_count['count'] ); ?>
|
||||
</span>
|
||||
<span class="screen-reader-text">
|
||||
<?php esc_html( $screen_reader_text ); ?>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div data-wc-interactive='<?php echo esc_attr( $data_directive ); ?>'>
|
||||
<?php if ( 'list' === $display_style ) : ?>
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $stock_counts as $stock_count ) { ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $stock_count['status'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
value="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
<?php checked( strpos( $selected_stock_status, $stock_count['status'] ) !== false, 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php echo esc_html( $stock_statuses[ $stock_count['status'] ] ); ?>
|
||||
|
||||
<?php if ( 'dropdown' === $display_style ) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dropdown::render() escapes output.
|
||||
echo Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'actions.filters.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
<?php if ( $show_counts ) : ?>
|
||||
<?php
|
||||
// translators: %s: number of products.
|
||||
$screen_reader_text = sprintf( _n( '%s product', '%s products', $stock_count['count'], 'woo-gutenberg-products-block' ), number_format_i18n( $stock_count['count'] ) );
|
||||
?>
|
||||
<span>
|
||||
<span aria-hidden="true">
|
||||
<?php $show_counts ? print( esc_html( '(' . $stock_count['count'] . ')' ) ) : null; ?>
|
||||
</span>
|
||||
<span class="screen-reader-text">
|
||||
<?php esc_html( $screen_reader_text ); ?>
|
||||
</span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'dropdown' === $display_style ) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dropdown::render() escapes output.
|
||||
echo Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'woocommerce/collection-stock-filter::actions.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
|
|
|
@ -23,14 +23,12 @@ class Dropdown {
|
|||
);
|
||||
|
||||
$dropdown_context = array(
|
||||
'woocommerceDropdown' => array(
|
||||
'selectedItem' => $selected_item,
|
||||
'hoveredItem' => array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
),
|
||||
'isOpen' => false,
|
||||
'selectedItem' => $selected_item,
|
||||
'hoveredItem' => array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
),
|
||||
'isOpen' => false,
|
||||
);
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
|
@ -40,45 +38,46 @@ class Dropdown {
|
|||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-block-stock-filter style-dropdown" data-wc-context='<?php echo wp_json_encode( $dropdown_context ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
data-wc-class--is-active="context.woocommerceDropdown.isOpen"
|
||||
tabindex="-1"
|
||||
data-wc-on--click="actions.woocommerceDropdown.toggleIsOpen"
|
||||
>
|
||||
<input id="components-form-token-input-1" type="text" autocomplete="off" data-wc-bind--placeholder="selectors.woocommerceDropdown.placeholderText" class="components-form-token-field__input" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-describedby="components-form-token-suggestions-howto-1" value="">
|
||||
<ul hidden data-wc-bind--hidden="!context.woocommerceDropdown.isOpen" class="components-form-token-field__suggestions-list" id="components-form-token-suggestions-1" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array(
|
||||
'woocommerceDropdown' => array( 'currentItem' => $item ),
|
||||
JSON_NUMERIC_CHECK,
|
||||
);
|
||||
?>
|
||||
<li
|
||||
role="option"
|
||||
data-wc-on--click--select-item="actions.woocommerceDropdown.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="selectors.woocommerceDropdown.isSelected"
|
||||
data-wc-on--mouseover="actions.woocommerceDropdown.addHoverClass"
|
||||
data-wc-on--mouseout="actions.woocommerceDropdown.removeHoverClass"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="selectors.woocommerceDropdown.isSelected"
|
||||
<div data-wc-interactive='<?php echo wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) ); ?>'>
|
||||
<div class="wc-block-stock-filter style-dropdown" data-wc-context='<?php echo wp_json_encode( $dropdown_context ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
data-wc-class--is-active="context.isOpen"
|
||||
tabindex="-1"
|
||||
data-wc-on--click="actions.toggleIsOpen"
|
||||
>
|
||||
<?php echo esc_html( $item['label'] ); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<input id="components-form-token-input-1" type="text" autocomplete="off" data-wc-bind--placeholder="selectors.placeholderText" class="components-form-token-field__input" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-describedby="components-form-token-suggestions-howto-1" value="">
|
||||
<ul hidden data-wc-bind--hidden="!context.isOpen" class="components-form-token-field__suggestions-list" id="components-form-token-suggestions-1" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array(
|
||||
'currentItem' => $item,
|
||||
);
|
||||
?>
|
||||
<li
|
||||
role="option"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="selectors.isSelected"
|
||||
data-wc-on--mouseover="actions.addHoverClass"
|
||||
data-wc-on--mouseout="actions.removeHoverClass"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="selectors.isSelected"
|
||||
>
|
||||
<?php echo esc_html( $item['label'] ); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue