Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated CTA Node Component into Koenig-Lexical #1413

Merged
merged 9 commits into from
Jan 22, 2025
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}
Comment on lines +40 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement missing handler functions.

Several handler functions are passed as empty arrow functions. These need to be implemented to provide the expected functionality:

  • handleButtonColor
  • handleColorChange
  • updateButtonText
  • updateButtonUrl
  • updateHasSponsorLabel
  • updateLayout
  • updateShowButton

Also applies to: 53-57

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
Loading