Skip to content

Commit

Permalink
Integrated CTA Node Component into Koenig-Lexical (#1413)
Browse files Browse the repository at this point in the history
ref https://linear.app/ghost/issue/PLG-310/configure-lexical-nodes-for-new-cta-card https://linear.app/ghost/issue/PLG-311/wire-up-ui-components-to-cta-nodes

- Started integrating the CTA node into Koenig Lexical
- Wired up the initial UI component to access the new `call-to-action` card from the koenig selector.
- Added the visibility of the CTA card behind a flag.
- Added test to check that the card can be rendered. At the moment it's static and features need to build on top of it.
  • Loading branch information
ronaldlangeveld authored Jan 22, 2025
1 parent 885a303 commit 31ad767
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to-
{name: 'showButton', default: false},
{name: 'buttonText', default: ''},
{name: 'buttonUrl', default: ''},
{name: 'buttonColor', default: ''},
{name: 'hasSponsorLabel', default: false},
{name: 'hasBackground', default: false},
{name: 'backgroundColor', default: '#123456'},
{name: 'backgroundColor', default: 'none'},
{name: 'hasImage', default: false},
{name: 'imageUrl', default: ''}
]}
Expand All @@ -22,6 +23,7 @@ export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to-
showButton,
buttonText,
buttonUrl,
buttonColor,
hasSponsorLabel,
hasBackground,
backgroundColor,
Expand All @@ -34,9 +36,10 @@ export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to-
this.__showButton = showButton || false;
this.__buttonText = buttonText || '';
this.__buttonUrl = buttonUrl || '';
this.__buttonColor = buttonColor || 'none';
this.__hasSponsorLabel = hasSponsorLabel || false;
this.__hasBackground = hasBackground || false;
this.__backgroundColor = backgroundColor || '#123456';
this.__backgroundColor = backgroundColor || 'none';
this.__hasImage = hasImage || false;
this.__imageUrl = imageUrl || '';
}
Expand Down
10 changes: 8 additions & 2 deletions packages/kg-default-nodes/test/nodes/call-to-action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ describe('CallToActionNode', function () {
showButton: true,
buttonText: 'click me',
buttonUrl: 'http://blog.com/post1',
buttonColor: 'none',
hasSponsorLabel: true,
hasBackground: true,
backgroundColor: '#123456',
backgroundColor: 'none',
hasImage: true,
imageUrl: 'http://blog.com/image1.jpg'
};
Expand All @@ -58,6 +59,7 @@ describe('CallToActionNode', function () {
callToActionNode.showButton.should.equal(dataset.showButton);
callToActionNode.buttonText.should.equal(dataset.buttonText);
callToActionNode.buttonUrl.should.equal(dataset.buttonUrl);
callToActionNode.buttonColor.should.equal(dataset.buttonColor);
callToActionNode.hasSponsorLabel.should.equal(dataset.hasSponsorLabel);
callToActionNode.hasBackground.should.equal(dataset.hasBackground);
callToActionNode.backgroundColor.should.equal(dataset.backgroundColor);
Expand Down Expand Up @@ -88,13 +90,17 @@ describe('CallToActionNode', function () {
callToActionNode.buttonUrl = 'http://blog.com/post1';
callToActionNode.buttonUrl.should.equal('http://blog.com/post1');

callToActionNode.buttonColor.should.equal('none');
callToActionNode.buttonColor = 'red';
callToActionNode.buttonColor.should.equal('red');

callToActionNode.hasSponsorLabel.should.equal(false);
callToActionNode.hasSponsorLabel = true;

callToActionNode.hasBackground.should.equal(false);
callToActionNode.hasBackground = true;

callToActionNode.backgroundColor.should.equal('#123456');
callToActionNode.backgroundColor.should.equal('none');
callToActionNode.backgroundColor = '#654321';
callToActionNode.backgroundColor.should.equal('#654321');

Expand Down
3 changes: 2 additions & 1 deletion packages/koenig-lexical/demo/DemoApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const defaultCardConfig = {
feature: {
collections: true,
collectionsCard: true,
contentVisibility: true
contentVisibility: true,
contentVisibilityAlpha: true
},
deprecated: {
headerV1: process.env.NODE_ENV === 'test' ? false : true // show header v1 only for tests
Expand Down
34 changes: 17 additions & 17 deletions packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,26 @@ export const ctaColorPicker = [
];

export function CtaCard({
buttonText,
buttonUrl,
buttonColor,
buttonText, //
buttonUrl, //
buttonColor, //
buttonTextColor,
color,
hasSponsorLabel,
color, //
hasSponsorLabel, //
htmlEditor,
htmlEditorInitialState,
imageSrc,
isEditing,
isSelected,
layout,
showButton,
updateButtonText,
updateButtonUrl,
updateShowButton,
updateHasSponsorLabel,
updateLayout,
handleColorChange,
handleButtonColor
imageSrc, //
isEditing, //
isSelected, //
layout, //
showButton, //
updateButtonText, //
updateButtonUrl, //
updateShowButton, //
updateHasSponsorLabel, //
updateLayout, //
handleColorChange, //
handleButtonColor //
}) {
const [buttonColorPickerExpanded, setButtonColorPickerExpanded] = useState(false);

Expand Down
2 changes: 2 additions & 0 deletions packages/koenig-lexical/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import KoenigNestedComposer from './components/KoenigNestedComposer';

/* Plugins */
import AudioPlugin from './plugins/AudioPlugin';
import CallToActionPlugin from './plugins/CallToActionPlugin';
import CalloutPlugin from './plugins/CalloutPlugin';
import CardMenuPlugin from './plugins/CardMenuPlugin';
import CollectionPlugin from './plugins/CollectionPlugin';
Expand Down Expand Up @@ -65,6 +66,7 @@ export {

AudioPlugin,
CalloutPlugin,
CallToActionPlugin,
CardMenuPlugin,
CollectionPlugin,
DragDropPastePlugin,
Expand Down
81 changes: 81 additions & 0 deletions packages/koenig-lexical/src/nodes/CallToActionNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import CalloutCardIcon from '../assets/icons/kg-card-type-callout.svg?react';
import KoenigCardWrapper from '../components/KoenigCardWrapper';
import {BASIC_NODES} from '../index.js';
import {CallToActionNode as BaseCallToActionNode} from '@tryghost/kg-default-nodes';
import {CallToActionNodeComponent} from './CallToActionNodeComponent';
import {createCommand} from 'lexical';
import {populateNestedEditor, setupNestedEditor} from '../utils/nested-editors';

export const INSERT_CTA_COMMAND = createCommand();

export class CallToActionNode extends BaseCallToActionNode {
__htmlEditor;
__htmlEditorInitialState;
// TODO: Improve the copy of the menu item
static kgMenu = {
label: 'Call to Action',
desc: 'Add a call to action to your post',
Icon: CalloutCardIcon, // TODO: Replace with correct icon
insertCommand: INSERT_CTA_COMMAND,
matches: ['cta', 'call-to-action'],
priority: 10,
shortcut: '/cta',
isHidden: ({config}) => {
return !(config?.feature?.contentVisibilityAlpha === true);
}
};

static getType() {
return 'call-to-action';
}

getIcon() {
// TODO: replace with correct icon
return CalloutCardIcon;
}

constructor(dataset = {}, key) {
super(dataset, key);

// set up nested editor instances
setupNestedEditor(this, '__htmlEditor', {editor: dataset.htmlEditor, nodes: BASIC_NODES});

// populate nested editors on initial construction
if (!dataset.htmlEditor) {
populateNestedEditor(this, '__htmlEditor', dataset.html || '<p>Hey <code>{first_name, "there"}</code>,</p>');
}
}

decorate() {
return (
<KoenigCardWrapper
nodeKey={this.getKey()}
wrapperStyle="wide"
>
<CallToActionNodeComponent
backgroundColor={this.backgroundColor}
buttonColor={this.buttonColor}
buttonText={this.buttonText}
buttonUrl={this.buttonUrl}
hasBackground={this.hasBackground}
hasImage={this.hasImage}
hasSponsorLabel={this.hasSponsorLabel}
htmlEditor={this.__htmlEditor}
imageUrl={this.imageUrl}
layout={this.layout}
nodeKey={this.getKey()}
showButton={this.showButton}
textValue={this.textValue}
/>
</KoenigCardWrapper>
);
}
}

export function $createCallToActionNode(dataset) {
return new CallToActionNode(dataset);
}

export function $isCallToActionNode(node) {
return node instanceof CallToActionNode;
}
85 changes: 85 additions & 0 deletions packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import CardContext from '../context/CardContext';
import KoenigComposerContext from '../context/KoenigComposerContext.jsx';
import React from 'react';
import {ActionToolbar} from '../components/ui/ActionToolbar.jsx';
import {CtaCard} from '../components/ui/cards/CtaCard';
import {SnippetActionToolbar} from '../components/ui/SnippetActionToolbar.jsx';
import {ToolbarMenu, ToolbarMenuItem, ToolbarMenuSeparator} from '../components/ui/ToolbarMenu.jsx';

export const CallToActionNodeComponent = ({
nodeKey,
backgroundColor,
buttonText,
buttonUrl,
hasBackground,
hasImage,
hasSponsorLabel,
imageUrl,
layout,
showButton,
textValue,
buttonColor,
htmlEditor
}) => {
// const [editor] = useLexicalComposerContext();
const {isEditing, isSelected, setEditing} = React.useContext(CardContext);
const {cardConfig} = React.useContext(KoenigComposerContext);
const [showSnippetToolbar, setShowSnippetToolbar] = React.useState(false);
const handleToolbarEdit = (event) => {
event.preventDefault();
event.stopPropagation();
setEditing(true);
};
return (
<>
<CtaCard
buttonColor={buttonColor}
buttonText={buttonText}
buttonUrl={buttonUrl}
color={backgroundColor}
handleButtonColor={() => {}}
handleColorChange={() => {}}
hasBackground={hasBackground}
hasImage={hasImage}
hasSponsorLabel={hasSponsorLabel}
htmlEditor={htmlEditor}
imageSrc={imageUrl}
isEditing={isEditing}
isSelected={isSelected}
layout={layout}
setEditing={setEditing}
showButton={showButton}
text={textValue}
updateButtonText={() => {}}
updateButtonUrl={() => {}}
updateHasSponsorLabel={() => {}}
updateLayout={() => {}}
updateShowButton={() => {}}
/>
<ActionToolbar
data-kg-card-toolbar="button"
isVisible={showSnippetToolbar}
>
<SnippetActionToolbar onClose={() => setShowSnippetToolbar(false)} />
</ActionToolbar>

<ActionToolbar
data-kg-card-toolbar="button"
isVisible={isSelected && !isEditing}
>
<ToolbarMenu>
<ToolbarMenuItem dataTestId="edit-button-card" icon="edit" isActive={false} label="Edit" onClick={handleToolbarEdit} />
<ToolbarMenuSeparator hide={!cardConfig.createSnippet} />
<ToolbarMenuItem
dataTestId="create-snippet"
hide={!cardConfig.createSnippet}
icon="snippet"
isActive={false}
label="Save as snippet"
onClick={() => setShowSnippetToolbar(true)}
/>
</ToolbarMenu>
</ActionToolbar>
</>
);
};
2 changes: 2 additions & 0 deletions packages/koenig-lexical/src/nodes/DefaultNodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {AsideNode} from './AsideNode';
import {AudioNode} from './AudioNode';
import {BookmarkNode} from './BookmarkNode';
import {ButtonNode} from './ButtonNode';
import {CallToActionNode} from './CallToActionNode';
import {CalloutNode} from './CalloutNode';
import {CodeBlockNode} from './CodeBlockNode';
import {CollectionNode} from './CollectionNode';
Expand Down Expand Up @@ -60,6 +61,7 @@ const DEFAULT_NODES = [
HtmlNode,
FileNode,
ButtonNode,
CallToActionNode,
ToggleNode,
HeaderNode,
BookmarkNode,
Expand Down
2 changes: 2 additions & 0 deletions packages/koenig-lexical/src/plugins/AllDefaultPlugins.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AtLinkPlugin from './AtLinkPlugin.jsx';
import CallToActionPlugin from '../plugins/CallToActionPlugin';
import CollectionPlugin from '../plugins/CollectionPlugin';
import EmEnDashPlugin from '../plugins/EmEnDashPlugin';
import HorizontalRulePlugin from '../plugins/HorizontalRulePlugin';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const AllDefaultPlugins = () => {
<EmbedPlugin />
<SignupPlugin />
<CollectionPlugin />
<CallToActionPlugin />
</>
);
};
Expand Down
33 changes: 33 additions & 0 deletions packages/koenig-lexical/src/plugins/CallToActionPlugin.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import {$createCallToActionNode, CallToActionNode, INSERT_CTA_COMMAND} from '../nodes/CallToActionNode';
import {COMMAND_PRIORITY_LOW} from 'lexical';
import {INSERT_CARD_COMMAND} from './KoenigBehaviourPlugin';
import {mergeRegister} from '@lexical/utils';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

export const CallToActionPlugin = () => {
const [editor] = useLexicalComposerContext();

React.useEffect(() => {
if (!editor.hasNodes([CallToActionNode])){
console.error('CallToActionPlugin: CallToActionNode not registered'); // eslint-disable-line no-console
return;
}
return mergeRegister(
editor.registerCommand(
INSERT_CTA_COMMAND,
async (dataset) => {
const cardNode = $createCallToActionNode(dataset);
editor.dispatchCommand(INSERT_CARD_COMMAND, {cardNode, openInEditMode: true});

return true;
},
COMMAND_PRIORITY_LOW
)
);
}, [editor]);

return null;
};

export default CallToActionPlugin;
Loading

0 comments on commit 31ad767

Please sign in to comment.