import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { ConditionalWrapper } from '@gravityforms/react-utils';
import { spacerClasses, clipboard } from '@gravityforms/utils';
import Button from '../Button';
const { forwardRef, useEffect, useRef } = React;
/**
* @module Heading
* @description The heading component with optional editability and a copy-to-clipboard button.
* When editable is true, the heading becomes editable on focus, with accessibility and onChange support.
* Optionally, a hidden input can store the value for form submission, and a button can copy content to the clipboard inline.
*
* @since 1.1.15
*
* @param {object} props Component props.
* @param {JSX.Element} props.children React element children (ignored when editable).
* @param {string} props.content The text content.
* @param {object} props.copyButtonAttributes Custom attributes for the copy button.
* @param {string|Array|object} props.copyButtonClasses Custom classes for the copy button.
* @param {object} props.customAttributes Custom attributes for the component.
* @param {string|Array|object} props.customClasses Custom classes for the component.
* @param {boolean} props.editable Whether the heading is editable on focus (default false).
* @param {string} props.id Optional id attribute when editable.
* @param {string} props.name Optional name attribute when editable.
* @param {Function} props.onBlur Handler for blur event when editable.
* @param {Function} props.onChange Handler for text changes when editable.
* @param {Function} props.onCopy Handler for copy event when editable.
* @param {Function} props.onFocus Handler for focus event when editable.
* @param {string} props.placeholder Placeholder text when editable and empty (default empty string).
* @param {boolean} props.showCopyButton Whether to show a button to copy the content to the clipboard (default false).
* @param {string} props.size The font size for the heading.
* @param {string|number|Array|object} props.spacing The spacing for the component.
* @param {string} props.tagName The tag used for the heading, from `h1` to `h6`.
* @param {string} props.type The type of the heading, one of `regular` or `boxed`.
* @param {boolean} props.useHiddenInput Whether to include a hidden input for form submission (default false).
* @param {string} props.weight The font weight for the heading.
* @param {object|null} ref Ref to the component.
*
* @return {JSX.Element} The heading component.
*
* @example
* // With copy button inline, non-editable
* <Heading tagName="h2" content="Copy Me" showCopyButton />
*
* // Controlled usage with copy button inline
* const [heading, setHeading] = useState("Editable Heading");
* <Heading tagName="h2" content={heading} editable onChange={setHeading} showCopyButton />
*
* // Self-managed form usage with copy button inline
* <form>
* <Heading tagName="h2" content="Editable Heading" editable useHiddenInput name="heading" id="heading-1" showCopyButton />
* <button type="submit">Submit</button>
* </form>
*
*/
const Heading = forwardRef( ( {
children = null,
content = '',
copyButtonAttributes = {},
copyButtonClasses = [],
customAttributes = {},
customClasses = [],
editable = false,
editButtonAttributes = {},
editButtonClasses = [],
id = '',
name = '',
onBlur = () => {},
onChange = () => {},
onCopy = () => {},
onFocus = () => {},
placeholder = '',
showCopyButton = false,
size = 'display-3xl',
spacing = '',
tagName = 'h1',
type = 'regular',
useHiddenInput = false,
weight = 'semibold',
}, ref ) => {
const internalRef = useRef( null );
const combinedRef = ref || internalRef;
const hiddenInputRef = useRef( null );
const headingAttributes = {
className: classnames( {
'gform-heading': true,
'gform-heading--editable': editable,
'gform-heading--has-copy': showCopyButton,
[ `gform-typography--size-${ size }` ]: true,
[ `gform-typography--weight-${ weight }` ]: true,
[ `gform-heading--${ type }` ]: true,
...spacerClasses( spacing ),
}, customClasses ),
ref: combinedRef,
...customAttributes,
};
const handleCopy = () => {
const textToCopy = editable ? combinedRef.current.textContent : content;
clipboard( textToCopy );
onCopy();
};
const handleEdit = () => {
combinedRef.current.focus();
};
const editButtonProps = {
customClasses: classnames( {
'gform-heading__edit-button': true,
[ `gform-typography--size-${ size }` ]: true,
}, editButtonClasses ),
icon: 'edit',
iconPrefix: 'gravity-component-icon',
onClick: handleEdit,
type: 'unstyled',
...editButtonAttributes,
};
const copyButtonProps = {
customClasses: classnames( {
'gform-heading__copy-button': true,
[ `gform-typography--size-${ size }` ]: true,
}, copyButtonClasses ),
icon: 'copy-alt',
iconPrefix: 'gravity-component-icon',
onClick: handleCopy,
type: 'unstyled',
...copyButtonAttributes,
};
const editableWithCopyWrapperProps = {
className: classnames( {
'gform-heading__wrapper': true,
}, [] ),
};
if ( editable ) {
headingAttributes.contentEditable = true;
headingAttributes.placeholder = placeholder;
headingAttributes.tabIndex = 0;
headingAttributes.role = 'textbox';
if ( ! useHiddenInput ) {
if ( name ) {
headingAttributes.name = name;
}
if ( id ) {
headingAttributes.id = id;
}
}
if ( ! onChange && ! useHiddenInput ) {
console.warn( 'Heading component: when editable is true and useHiddenInput is false, onChange prop is recommended for controlled behavior.' );
}
}
useEffect( () => {
if ( editable && combinedRef.current ) {
const element = combinedRef.current;
if ( element.textContent !== content ) {
element.textContent = content;
}
const handleInput = () => {
const newText = element.textContent;
if ( newText.trim() === '' ) {
element.textContent = content;
}
if ( onChange ) {
onChange( newText );
}
if ( useHiddenInput && hiddenInputRef.current ) {
hiddenInputRef.current.value = newText;
}
};
const handleBlur = () => {
if ( element.textContent.trim() === '' ) {
element.textContent = content;
}
onBlur( element.textContent );
};
const handleFocus = () => {
onFocus( element.textContent );
};
element.addEventListener( 'input', handleInput );
element.addEventListener( 'blur', handleBlur );
element.addEventListener( 'focus', handleFocus );
return () => {
element.removeEventListener( 'input', handleInput );
element.removeEventListener( 'blur', handleBlur );
element.removeEventListener( 'focus', handleFocus );
};
}
}, [ editable, content, onChange, useHiddenInput, combinedRef, onBlur, onFocus ] );
const Container = tagName;
const copyButton = showCopyButton ? (
<Button { ...copyButtonProps } />
) : null;
const editButton = editable ? (
<Button { ...editButtonProps } />
) : null;
if ( editable ) {
return (
<>
<ConditionalWrapper
condition={ showCopyButton || editable }
wrapper={ ( ch ) => <div { ...editableWithCopyWrapperProps }>{ ch }</div> }
>
<Container { ...headingAttributes } />
{ editButton }
{ copyButton }
</ConditionalWrapper>
{ useHiddenInput && (
<input
type="hidden"
ref={ hiddenInputRef }
name={ name }
id={ id }
defaultValue={ content }
/>
) }
</>
);
}
return (
<Container { ...headingAttributes }>
{ content }
{ children }
{ copyButton }
</Container>
);
} );
Heading.propTypes = {
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ),
content: PropTypes.string,
copyButtonAttributes: PropTypes.object,
copyButtonClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
customAttributes: PropTypes.object,
customClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
editable: PropTypes.bool,
id: PropTypes.string,
name: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onCopy: PropTypes.func,
onFocus: PropTypes.func,
placeholder: PropTypes.string,
showCopyButton: PropTypes.bool,
size: PropTypes.string,
spacing: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.object,
] ),
tagName: PropTypes.string,
type: PropTypes.string,
useHiddenInput: PropTypes.bool,
weight: PropTypes.string,
};
Heading.displayName = 'Heading';
export default Heading;