File: /storage/v4513/tepnot/public_html/wp-content/plugins/echo-knowledge-base/js/blocks/components.js
import { useState, useEffect } from '@wordpress/element';
import { RangeControl, TextControl, Panel, TabPanel, PanelBody, ToggleControl, FontSizePicker, CustomSelectControl, CheckboxControl, BaseControl } from '@wordpress/components';
import {
__experimentalNumberControl as NumberControl,
__experimentalToggleGroupControl as ToggleGroupControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
__experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon,
__experimentalBoxControl as BoxControl,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem
} from '@wordpress/components';
import {
InspectorControls,
PanelColorSettings,
useBlockProps,
__experimentalFontFamilyControl as FontFamilyControl
} from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
import { useDispatch } from '@wordpress/data';
import { show_kb_block_page_template } from "./utils";
export default function EpkbInspectorControls({ block_ui_config, attributes, setAttributes, blockName }) {
const { select } = wp.data;
const { editPost } = useDispatch('core/editor');
const coreStore = select('core');
const editorStore = select('core/editor');
const pageTemplate = editorStore.getEditedPostAttribute('template');
const currentlyStoredTemplate = coreStore.getEntityRecord('postType', editorStore.getCurrentPostType(), editorStore.getCurrentPostId())?.template;
const isTemplateToggleChecked = pageTemplate === block_ui_config.settings.kb_block_page_template;
// To store temporary values of block without actually storing the value in the block attributes (e.g. like selected presets)
const [epkbBlockStorage, setEpkbBlockStorage] = useState({});
// A set of all field names which are currently used for the current block UI and the field types (to detect whether dependency field is actually present in the UI or is only present in attributes)
const currentUiFieldTypes = Object.fromEntries(
Object.values(block_ui_config)
.filter(tab => tab.groups)
.flatMap(tab => Object.values(tab.groups))
.filter(group => group.fields)
.flatMap(group => Object.entries(group.fields))
.map(([field_name, field_config]) => [field_name, field_config.setting_type])
);
// Disable links inside blocks content
const disableLinksInsideBlockContent = (event) => {
const link = event.target.closest('a');
if (link && link.closest('.eckb-block-editor-preview') && ! link.closest('.eckb-kb-no-content') && ! link.closest('#eckb-kb-faqs-not-assigned')) {
event.preventDefault();
}
};
const blockProps = useBlockProps({
onClickCapture: disableLinksInsideBlockContent,
});
function ensure_number(input_value) {
return typeof input_value === 'number' ? input_value : parseInt(input_value)
}
function check_dependency(field_specs) {
let show_field = true;
// Hide the current field when any of the target fields has certain value
if (field_specs.hide_on_dependencies) {
const hide_on_dependencies = Object.entries(field_specs.hide_on_dependencies);
hide_on_dependencies.forEach(([dependency_name, dependency_value]) => {
if (dependency_name in currentUiFieldTypes && currentUiFieldTypes[dependency_name].length && attributes[dependency_name] === dependency_value) {
show_field = false;
}
});
}
// Hide the current field when any of the target selection controls has certain amount of selected options
if (field_specs.hide_on_selection_amount_dependencies) {
const hide_on_selection_amount_dependencies = Object.entries(field_specs.hide_on_selection_amount_dependencies);
hide_on_selection_amount_dependencies.forEach(([dependency_name, dependency_amount]) => {
if (dependency_name in currentUiFieldTypes && currentUiFieldTypes[dependency_name].length && attributes[dependency_name].length === dependency_amount) {
show_field = false;
}
});
}
return show_field;
}
function is_disabled_by_dependency(field_specs) {
let disabled = false;
// Disable on
if (field_specs.disable_on_dependencies) {
const disable_on_dependencies = Object.entries(field_specs.disable_on_dependencies);
disable_on_dependencies.forEach(([dependency_name, dependency_value]) => {
if (attributes[dependency_name] === dependency_value) {
disabled = true;
}
});
}
return disabled;
}
// show KB block page template by removing inline CSS which hides it (running each time when this block is added or edited)
show_kb_block_page_template();
// Effect to synchronize template_toggle attributes after render
useEffect(() => {
// Find all template_toggle fields
const templateToggleFields = Object.values(block_ui_config)
.filter(tab => tab.groups)
.flatMap(tab => Object.values(tab.groups))
.filter(group => group.fields)
.flatMap(group => Object.entries(group.fields))
.filter(([, fconf]) => fconf.setting_type === 'template_toggle')
.map(([fname]) => fname);
templateToggleFields.forEach((toggleFieldName) => {
if (isTemplateToggleChecked && attributes[toggleFieldName] !== 'on') {
setAttributes({ [toggleFieldName]: 'on' });
} else if (!isTemplateToggleChecked && attributes[toggleFieldName] === 'on') {
setAttributes({ [toggleFieldName]: 'off' });
}
});
}, [isTemplateToggleChecked, attributes, block_ui_config, setAttributes]);
return (
<div {...blockProps}>
<InspectorControls>
<Panel className="epkb-block-editor-controls">
<TabPanel
className="epkb-block-editor-tabpanel"
tabs={Object.entries(block_ui_config).map(([tab_name, tab_config]) => {
return {
name: tab_name,
title: tab_config.title,
icon: tab_config.icon,
groups: tab_config.groups,
};
})}
>{(tab) => {
// Tab
return <React.Fragment>{
// Groups in the current tab
Object.entries(tab.groups).map(([group_key, {title, fields}]) => {
// Filter fields for the current group by their dependencies
const filteredFields = Object.entries(fields).filter(([field_name, field_specs]) => check_dependency(field_specs));
// Do not display the current group if it currently does not have any field
if (!filteredFields.length) {
return null;
}
const GroupKeyClass =
typeof group_key === "string" && group_key.trim() !== ""
? " epkb-"+group_key
: "";
return <PanelBody key={group_key} title={title} className={"epkb-block-ui-section" + GroupKeyClass}>{
// Controls in the current group
filteredFields.map(([field_name, field_specs]) => {
if (!check_dependency(field_specs)) {
return null;
}
const isDisabled = is_disabled_by_dependency(field_specs);
switch (field_specs.setting_type) {
case 'text':
return <TextControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
disabled={isDisabled}
label={field_specs.label}
value={attributes[field_name]}
onChange={(value) => setAttributes({[field_name]: value})}
help={field_specs.description || ''}
className="epkb-block-ui-text-control"
/>;
case 'number':
return <NumberControl
key={field_name}
__next40pxDefaultSize={true}
disabled={isDisabled}
isShiftStepEnabled={true}
shiftStep={1}
min={field_specs.min}
max={field_specs.max}
label={field_specs.label}
value={(attributes[field_name] === "" || Number.isNaN(parseInt(attributes[field_name]))) ? 1 : parseInt(attributes[field_name])}
onChange={(value) => setAttributes({[field_name]: (value === "" || Number.isNaN(parseInt(value))) ? 1 : parseInt(value)})}
className="epkb-block-ui-number-control"
/>;
case 'color':
// epkb-block-editor-controls
return <PanelColorSettings
key={field_name}
colorSettings={[{
value: attributes[field_name],
onChange: (newColor) => setAttributes({[field_name]: newColor}),
enableAlpha: true,
label: field_specs.label,
}]}
className="epkb-block-ui-color-control"
/>;
case 'select_buttons_string':
return <ToggleGroupControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
isBlock
label={field_specs.label}
onChange={(value) => setAttributes({[field_name]: value})}
value={typeof attributes[field_name] !== 'undefined' ? attributes[field_name] : field_specs.default}
className="epkb-block-ui-select-buttons-control"
>
{Object.entries(field_specs.options).map(([option_value, option_label]) => {
return <ToggleGroupControlOption
key={option_value}
label={option_label}
value={option_value}
/>
})}
</ToggleGroupControl>
case 'select_buttons_icon':
return <ToggleGroupControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
isBlock
label={field_specs.label}
onChange={(value) => setAttributes({[field_name]: value})}
value={typeof attributes[field_name] !== 'undefined' ? attributes[field_name] : field_specs.default}
className="epkb-block-ui-select-buttons-control"
>
{Object.entries(field_specs.options).map(([option_value, option_data]) => {
return <ToggleGroupControlOptionIcon
key={option_value}
icon={<span className={'epkbfa' + ' ' + option_data.icon_class}></span>}
label={option_data.label}
value={option_value}
/>
})}
</ToggleGroupControl>
case 'select_buttons':
return <ToggleGroupControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
isBlock
label={field_specs.label}
onChange={(value) => setAttributes({[field_name]: ensure_number(value)})}
value={typeof attributes[field_name] !== 'undefined' ? ensure_number(attributes[field_name]) : ensure_number(field_specs.default)}
className="epkb-block-ui-select-buttons-control"
>
{Object.entries(field_specs.options).map(([option_value, option_label]) => {
return <ToggleGroupControlOption
key={option_value}
label={option_label}
value={typeof option_value === 'number' ? option_value : parseInt(option_value)}
/>
})}
</ToggleGroupControl>
case 'toggle':
return <ToggleControl
key={field_name}
disabled={isDisabled}
__nextHasNoMarginBottom={true}
label={field_specs.label}
checked={attributes[field_name] === 'on'}
onChange={(value) => setAttributes({[field_name]: (value ? 'on' : 'off')})}
className="epkb-block-ui-toggle-control"
/>
case 'custom_toggle':
return <ToggleControl
key={field_name}
disabled={isDisabled}
__nextHasNoMarginBottom={true}
label={field_specs.label}
checked={attributes[field_name] === field_specs.options['on']}
onChange={(value) => setAttributes({[field_name]: (value ? field_specs.options['on'] : field_specs.options['off'])})}
className="epkb-block-ui-toggle-control"
/>
case 'template_toggle':
return <ToggleControl
key={field_name}
disabled={isDisabled}
__nextHasNoMarginBottom={true}
label={field_specs.label}
checked={isTemplateToggleChecked}
onChange={(value) => {
// Update value in the block attributes
setAttributes({[field_name]: (value ? 'on' : 'off')});
// Toggle is 'ON' - set the KB template
if ( value ) {
editPost({ template: block_ui_config.settings.kb_block_page_template });
return;
}
// Toggle is 'OFF' - set to the currently stored template or to the default one (if KB template is the stored template)
editPost({ template: currentlyStoredTemplate === block_ui_config.settings.kb_block_page_template ? '' : currentlyStoredTemplate });
}}
className="epkb-block-ui-toggle-control"
/>
case 'range':
return <RangeControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
disabled={isDisabled}
isShiftStepEnabled={true}
shiftStep={1}
min={field_specs.min}
max={field_specs.max}
label={field_specs.label}
value={(attributes[field_name] === "" || Number.isNaN(parseInt(attributes[field_name]))) ? parseInt(field_specs.default) : parseInt(attributes[field_name])}
onChange={(value) => setAttributes({[field_name]: (value === "" || Number.isNaN(parseInt(value))) ? 1 : parseInt(value)})}
className="epkb-block-ui-range-control"
help={<>
{field_specs.description && (<span className="epkb-help-description">{field_specs.description}</span>)}
{field_specs.help_text && (<span className="epkb-help-text">{field_specs.help_text}{" "}<a href={field_specs.help_link_url} target="_blank" rel="noopener noreferrer">{field_specs.help_link_text}</a></span>)}
</>}
/>
case 'range_float':
return <RangeControl
key={field_name}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
disabled={isDisabled}
isShiftStepEnabled={true}
shiftStep={0.05}
step={0.05}
min={field_specs.min}
max={field_specs.max}
label={field_specs.label}
value={(attributes[field_name] === "" || Number.isNaN(parseFloat(attributes[field_name]))) ? parseFloat(field_specs.default) : parseFloat(attributes[field_name])}
onChange={(value) => setAttributes({[field_name]: (value === "" || Number.isNaN(parseFloat(value))) ? parseFloat(field_specs.default).toFixed(2) : parseFloat(value).toFixed(2)})}
className="epkb-block-ui-range-control"
help={<>
{field_specs.description && (<span className="epkb-help-description">{field_specs.description}</span>)}
{field_specs.help_text && (<span className="epkb-help-text">{field_specs.help_text}{" "}<a href={field_specs.help_link_url} target="_blank" rel="noopener noreferrer">{field_specs.help_link_text}</a></span>)}
</>}
/>
case 'custom_dropdown':
const selectedKey = attributes[field_name] || field_specs.default;
const selectedOption = Object.values(field_specs.options).find(
(option) => option.key === selectedKey
);
return <CustomSelectControl
key={field_name}
disabled={isDisabled}
__next40pxDefaultSize={true}
label={field_specs.label}
value={selectedOption}
onChange={(value) => setAttributes({[field_name]: value.selectedItem.key})}
options={Object.entries(field_specs.options).map(([option_index, options_list]) => {
return {
key: options_list.key,
name: options_list.name,
style: options_list.style,
};
})}
className="epkb-block-ui-custom-dropdown-control"
/>
case 'dropdown':
return <CustomSelectControl
key={field_name}
disabled={isDisabled}
__next40pxDefaultSize={true}
label={field_specs.label}
value={{
key: attributes[field_name] || field_specs.default,
name: field_specs.options[attributes[field_name] || field_specs.default],
style: {},
}}
onChange={(value) => {
setAttributes({[field_name]: value.selectedItem.key});
// EL.AY sidebar components - deselect the same component if it is selected in another sidebar
/*switch (field_name) {
case 'nav_sidebar_left':
case 'kb_sidebar_left':
case 'toc_left':
setAttributes({[field_name.replace('_left', '_right')]: '0'});
break;
case 'nav_sidebar_right':
case 'kb_sidebar_right':
case 'toc_right':
setAttributes({[field_name.replace('_right', '_left')]: '0'});
break;
}*/
}}
options={Object.entries(field_specs.options).map(([option_key, options_name]) => {
return {
key: option_key,
name: options_name,
style: {},
};
})}
className="epkb-block-ui-dropdown-control"
/>
case 'presets_dropdown':
// Get current selected preset name from temporary storage
const selectedPresetKey = epkbBlockStorage[field_name] ? epkbBlockStorage[field_name] : field_specs.default;
return <CustomSelectControl
key={field_name}
disabled={isDisabled}
__next40pxDefaultSize={true}
label={field_specs.label}
value={{
key: selectedPresetKey,
name: field_specs.presets[selectedPresetKey].label,
style: {},
}}
onChange={(value) => {
// If user selected 'current' in presets dropdown, then do nothing
if (value.selectedItem.key === 'current') {
return;
}
// Update preset name in temporary storage (the preset value is not storing in the block attributes and only needed for editor preview)
setEpkbBlockStorage((prevState) => ({
...prevState,
[field_name]: value.selectedItem.key,
}));
// Apply preset settings for editor preview
Object.entries(field_specs.presets[value.selectedItem.key].settings).forEach(([setting_name, setting_value]) => {
setAttributes({
[setting_name]: setting_value
});
});
// Set preset name to make the non-block KB code working correctly (icons change for editor preview)
setAttributes({
theme_presets: value.selectedItem.key,
theme_name: value.selectedItem.key,
});
}}
options={Object.entries(field_specs.presets).map(([preset_key, preset_config]) => {
return {
key: preset_key,
name: preset_config.label,
style: {},
};
})}
className="epkb-block-ui-presets-dropdown-control"
/>
case 'checkbox_multi_select':
return <BaseControl
key={field_name}
label={field_specs.label}
__nextHasNoMarginBottom={true}
className="epkb-block-ui-checkbox-multi-select-control">{
Object.entries(field_specs.options).map(([option_key, option_value]) => {
return <CheckboxControl
key={field_name + '_' + option_key}
disabled={isDisabled}
__nextHasNoMarginBottom={true}
checked={attributes[field_name].indexOf(parseInt(option_key)) !== -1}
label={option_value}
onChange={(isChecked) => {
const intValue = parseInt(option_key);
const newValues = isChecked ? [...attributes[field_name], intValue] : attributes[field_name].filter(item => item !== intValue);
setAttributes({[field_name]: newValues});
}}
/>
})
}</BaseControl>
case 'box_control':
const sideValues = {[field_specs.side]: attributes[field_name] === "" || Number.isNaN(parseInt(attributes[field_name])) ? field_specs.default : parseInt(attributes[field_name])};
return <BoxControl
key={field_name}
__next40pxDefaultSize={true}
label={field_specs.label}
sides={[field_specs.side]}
inputProps={{
min: ensure_number(field_specs.min),
max: ensure_number(field_specs.max),
}}
values={sideValues}
onChange={(values) => {
const newValue = values[field_specs.side] || values;
setAttributes({
[field_name]: newValue === "" || Number.isNaN(parseInt(newValue))
? field_specs.default
: parseInt(newValue)
});
}}
className="epkb-block-ui-box-control"
/>
case 'box_control_combined':
const combinedSidesList = Object.values(field_specs.combined_settings).map((setting_specs) => {
return setting_specs.side
});
const combinedSideValues = Object.fromEntries(
Object.entries(field_specs.combined_settings).map(([setting_name, setting_specs]) => {
const combinedOneSideValue = attributes[setting_name] === "" || Number.isNaN(parseInt(attributes[setting_name])) ? setting_specs.default : parseInt(attributes[setting_name]);
return [setting_specs.side, combinedOneSideValue];
})
);
const defaultSideValues = Object.fromEntries(
Object.entries(field_specs.combined_settings).map(([setting_name, setting_specs]) => {
return [setting_specs.side, setting_specs.default]
})
);
return <BoxControl
key={field_name}
__next40pxDefaultSize={true}
label={field_specs.label}
sides={combinedSidesList}
resetValues={defaultSideValues}
inputProps={{
min: ensure_number(field_specs.min),
max: ensure_number(field_specs.max),
}}
values={combinedSideValues}
onChange={(values) => {
Object.entries(values).forEach(([side, value]) => {
Object.entries(field_specs.combined_settings).forEach(([setting_name, setting_specs]) => {
if (setting_specs.side === side) {
const newValue = typeof value === 'object' ? value.value : value;
setAttributes({
[setting_name]: newValue === "" || Number.isNaN(parseInt(newValue))
? field_specs.combined_settings[setting_name].default
: parseInt(newValue)
});
}
});
});
}}
className="epkb-block-ui-box-combined-control"
/>
case 'typography_controls':
return <ToolsPanel
key={field_name}
label={field_specs.label}
resetAll={() => {
setAttributes({
[field_name]: {
...attributes[field_name],
font_family: field_specs.controls.font_family.default,
font_size: field_specs.controls.font_size.default,
font_appearance: field_specs.controls.font_appearance.default,
},
});
}}
className="epkb-typography-controls"
>
<ToolsPanelItem
hasValue={() => { return attributes[field_name].font_family !== field_specs.controls.font_family.default; }}
label={field_specs.controls.font_family.label}
onDeselect={() => {
setAttributes({
[field_name]: {
...attributes[field_name],
font_family: field_specs.controls.font_family.default,
},
});
}}
>
<FontFamilyControl
key={field_name + '_font_family'}
__nextHasNoMarginBottom={true}
__next40pxDefaultSize={true}
fontFamilies={Object.entries(epkb_block_editor_vars.font_families).map(([option_index, options_value]) => {
return {
fontFamily: options_value,
name: options_value,
slug: options_value,
};
})}
onChange={(value) => {
const typographyControls = {
...attributes[field_name],
font_family: value,
};
setAttributes({[field_name]: typographyControls});
}}
value={attributes[field_name].font_family || field_specs.controls.font_family.default}
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={() => { return true; }}
label={field_specs.controls.font_size.label}
onDeselect={() => {
setAttributes({
[field_name]: {
...attributes[field_name],
font_size: field_specs.controls.font_size.default,
},
});
}}
isShownByDefault
>
<FontSizePicker
key={field_name + '_font_size'}
__next40pxDefaultSize={true}
fontSizes={Object.entries(field_specs.controls.font_size.options).map(([option_slug, options_list]) => {
return {
name: options_list.name,
size: options_list.size,
slug: option_slug,
};
})}
value={attributes[field_name].font_size || field_specs.controls.font_size.default}
onChange={(value) => {
const typographyControls = {
...attributes[field_name],
font_size: value,
};
setAttributes({[field_name]: typographyControls});
}}
disableCustomFontSizes={false}
withReset={false}
withSlider={true}
fallbackFontSize={field_specs.controls.font_size.default}
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={() => { return attributes[field_name].font_appearance !== field_specs.controls.font_appearance.default; }}
label={field_specs.controls.font_appearance.label}
onDeselect={() => {
setAttributes({
[field_name]: {
...attributes[field_name],
font_appearance: field_specs.controls.font_appearance.default,
},
});
}}
>
<CustomSelectControl
key={field_name + '_font_appearance'}
__next40pxDefaultSize={true}
label={field_specs.controls.font_appearance.label}
value={(() => {
const selectedKey = attributes[field_name].font_appearance || field_specs.controls.font_appearance.default;
const selectedOption = field_specs.controls.font_appearance.options[selectedKey];
return {
key: selectedKey,
...selectedOption,
};
})()}
onChange={(value) => {
const typographyControls = {
...attributes[field_name],
font_appearance: value.selectedItem.key,
};
setAttributes({[field_name]: typographyControls});
}}
options={Object.entries(field_specs.controls.font_appearance.options).map(([option_key, options_list]) => {
return {
key: option_key,
name: options_list.name,
style: options_list.style,
};
})}
/>
</ToolsPanelItem>
</ToolsPanel>
case 'section_description':
const linkUrl = field_specs.link_text.length > 0 ? field_specs.link_url.replaceAll('epkb_post_type_1', 'epkb_post_type_' + attributes['kb_id']) : '';
return <div key={field_name} className="epkb-block-ui-section-description">
<span>{field_specs.description}</span>
{field_specs.link_text.length > 0 ? (<a href={linkUrl} target="_blank" rel="noopener noreferrer">{field_specs.link_text}</a>) : null}
</div>;
default:
return null;
}
})
}</PanelBody>
})
}</React.Fragment>
}}
</TabPanel>
</Panel>
</InspectorControls>
<ServerSideRender
block={blockName}
attributes={attributes}
epkbBlockStorage={epkbBlockStorage} // ensure the preview is re-rendered when the local storage is updated
urlQueryArgs={{is_editor_preview: 1}} // Flag to indicate editor preview mode - retrieved in PHP via class-epkb-abstract-block.php:108
httpMethod="POST"
/>
</div>
);
}