feat: simplify TipTap button styling + add click-to-edit
Button Styling:
- Buttons now render as simple links with 🔘 prefix in editor
- No more styled button appearance in TipTap (was inconsistent)
- Actual button styling still happens in email (EmailRenderer.php)
Click-to-Edit:
- Click any button in the editor to open edit dialog
- Edit button text, link URL, and style (solid/outline)
- Delete button option in edit mode
- Updates button in-place instead of requiring recreation
Dialog improvements:
- Shows 'Edit Button' title in edit mode
- Shows 'Update Button' vs 'Insert Button' based on mode
- Delete button (red) appears only in edit mode
This commit is contained in:
@@ -76,14 +76,6 @@ export function RichTextEditor({
|
|||||||
class:
|
class:
|
||||||
'prose prose-sm max-w-none focus:outline-none min-h-[200px] px-4 py-3 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-3 [&_h2]:mb-2 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-2 [&_h3]:mb-1 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-2 [&_h4]:mb-1',
|
'prose prose-sm max-w-none focus:outline-none min-h-[200px] px-4 py-3 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-3 [&_h2]:mb-2 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-2 [&_h3]:mb-1 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-2 [&_h4]:mb-1',
|
||||||
},
|
},
|
||||||
handleClick: (view, pos, event) => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (target.tagName === 'A' || target.closest('a')) {
|
|
||||||
event.preventDefault();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,6 +113,8 @@ export function RichTextEditor({
|
|||||||
const [buttonText, setButtonText] = useState('Click Here');
|
const [buttonText, setButtonText] = useState('Click Here');
|
||||||
const [buttonHref, setButtonHref] = useState('{order_url}');
|
const [buttonHref, setButtonHref] = useState('{order_url}');
|
||||||
const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline'>('solid');
|
const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline'>('solid');
|
||||||
|
const [isEditingButton, setIsEditingButton] = useState(false);
|
||||||
|
const [editingButtonPos, setEditingButtonPos] = useState<number | null>(null);
|
||||||
|
|
||||||
const addImage = () => {
|
const addImage = () => {
|
||||||
openWPMediaImage((file) => {
|
openWPMediaImage((file) => {
|
||||||
@@ -136,12 +130,81 @@ export function RichTextEditor({
|
|||||||
setButtonText('Click Here');
|
setButtonText('Click Here');
|
||||||
setButtonHref('{order_url}');
|
setButtonHref('{order_url}');
|
||||||
setButtonStyle('solid');
|
setButtonStyle('solid');
|
||||||
|
setIsEditingButton(false);
|
||||||
|
setEditingButtonPos(null);
|
||||||
setButtonDialogOpen(true);
|
setButtonDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle clicking on buttons in the editor to edit them
|
||||||
|
const handleEditorClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const buttonEl = target.closest('a[data-button]') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (buttonEl && editor) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Get button attributes
|
||||||
|
const text = buttonEl.getAttribute('data-text') || buttonEl.textContent?.replace('🔘 ', '') || 'Click Here';
|
||||||
|
const href = buttonEl.getAttribute('data-href') || '#';
|
||||||
|
const style = (buttonEl.getAttribute('data-style') as 'solid' | 'outline') || 'solid';
|
||||||
|
|
||||||
|
// Find the position of this button node
|
||||||
|
const { state } = editor.view;
|
||||||
|
let foundPos: number | null = null;
|
||||||
|
|
||||||
|
state.doc.descendants((node, pos) => {
|
||||||
|
if (node.type.name === 'button' &&
|
||||||
|
node.attrs.text === text &&
|
||||||
|
node.attrs.href === href) {
|
||||||
|
foundPos = pos;
|
||||||
|
return false; // Stop iteration
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open dialog in edit mode
|
||||||
|
setButtonText(text);
|
||||||
|
setButtonHref(href);
|
||||||
|
setButtonStyle(style);
|
||||||
|
setIsEditingButton(true);
|
||||||
|
setEditingButtonPos(foundPos);
|
||||||
|
setButtonDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const insertButton = () => {
|
const insertButton = () => {
|
||||||
|
if (isEditingButton && editingButtonPos !== null && editor) {
|
||||||
|
// Delete old button and insert new one at same position
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 })
|
||||||
|
.insertContentAt(editingButtonPos, {
|
||||||
|
type: 'button',
|
||||||
|
attrs: { text: buttonText, href: buttonHref, style: buttonStyle },
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
} else {
|
||||||
|
// Insert new button
|
||||||
editor.chain().focus().setButton({ text: buttonText, href: buttonHref, style: buttonStyle }).run();
|
editor.chain().focus().setButton({ text: buttonText, href: buttonHref, style: buttonStyle }).run();
|
||||||
|
}
|
||||||
setButtonDialogOpen(false);
|
setButtonDialogOpen(false);
|
||||||
|
setIsEditingButton(false);
|
||||||
|
setEditingButtonPos(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteButton = () => {
|
||||||
|
if (editingButtonPos !== null && editor) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 })
|
||||||
|
.run();
|
||||||
|
setButtonDialogOpen(false);
|
||||||
|
setIsEditingButton(false);
|
||||||
|
setEditingButtonPos(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveHeading = () => {
|
const getActiveHeading = () => {
|
||||||
@@ -293,7 +356,9 @@ export function RichTextEditor({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
|
<div onClick={handleEditorClick}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Variables - Collapsible and Categorized */}
|
{/* Variables - Collapsible and Categorized */}
|
||||||
{variables.length > 0 && (
|
{variables.length > 0 && (
|
||||||
@@ -381,12 +446,20 @@ export function RichTextEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Button Dialog */}
|
{/* Button Dialog */}
|
||||||
<Dialog open={buttonDialogOpen} onOpenChange={setButtonDialogOpen}>
|
<Dialog open={buttonDialogOpen} onOpenChange={(open) => {
|
||||||
|
setButtonDialogOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setIsEditingButton(false);
|
||||||
|
setEditingButtonPos(null);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{__('Insert Button')}</DialogTitle>
|
<DialogTitle>{isEditingButton ? __('Edit Button') : __('Insert Button')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{__('Add a styled button to your content. Use variables for dynamic links.')}
|
{isEditingButton
|
||||||
|
? __('Edit the button properties below. Click on the button to save.')
|
||||||
|
: __('Add a styled button to your content. Use variables for dynamic links.')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -440,12 +513,17 @@ export function RichTextEditor({
|
|||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||||
|
{isEditingButton && (
|
||||||
|
<Button variant="destructive" onClick={deleteButton} className="sm:mr-auto">
|
||||||
|
{__('Delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
||||||
{__('Cancel')}
|
{__('Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={insertButton}>
|
<Button onClick={insertButton}>
|
||||||
{__('Insert Button')}
|
{isEditingButton ? __('Update Button') : __('Insert Button')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -66,45 +66,23 @@ export const ButtonExtension = Node.create<ButtonOptions>({
|
|||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
const { text, href, style } = HTMLAttributes;
|
const { text, href, style } = HTMLAttributes;
|
||||||
const className = style === 'outline' ? 'button-outline' : 'button';
|
|
||||||
|
|
||||||
const buttonStyle: Record<string, string> = style === 'solid'
|
|
||||||
? {
|
|
||||||
display: 'inline-block',
|
|
||||||
background: '#7f54b3',
|
|
||||||
color: '#fff',
|
|
||||||
padding: '14px 28px',
|
|
||||||
borderRadius: '6px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: '600',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
display: 'inline-block',
|
|
||||||
background: 'transparent',
|
|
||||||
color: '#7f54b3',
|
|
||||||
padding: '12px 26px',
|
|
||||||
border: '2px solid #7f54b3',
|
|
||||||
borderRadius: '6px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontWeight: '600',
|
|
||||||
cursor: 'pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Simple link styling - no fancy button appearance in editor
|
||||||
|
// The actual button styling happens in email rendering (EmailRenderer.php)
|
||||||
|
// In editor, just show as a link with visual indicator it's a button
|
||||||
return [
|
return [
|
||||||
'a',
|
'a',
|
||||||
mergeAttributes(this.options.HTMLAttributes, {
|
mergeAttributes(this.options.HTMLAttributes, {
|
||||||
href,
|
href,
|
||||||
class: className,
|
class: 'button-node',
|
||||||
style: Object.entries(buttonStyle)
|
style: 'color: #7f54b3; text-decoration: underline; cursor: pointer; font-weight: 500;',
|
||||||
.map(([key, value]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value}`)
|
|
||||||
.join('; '),
|
|
||||||
'data-button': '',
|
'data-button': '',
|
||||||
'data-text': text,
|
'data-text': text,
|
||||||
'data-href': href,
|
'data-href': href,
|
||||||
'data-style': style,
|
'data-style': style,
|
||||||
|
title: `Button: ${text} → ${href}`,
|
||||||
}),
|
}),
|
||||||
text,
|
`🔘 ${text}`,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user