diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js b/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js index 6f921848c6..ba20793584 100644 --- a/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js +++ b/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js @@ -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: ''} ]} @@ -22,6 +23,7 @@ export class CallToActionNode extends generateDecoratorNode({nodeType: 'call-to- showButton, buttonText, buttonUrl, + buttonColor, hasSponsorLabel, hasBackground, backgroundColor, @@ -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 || ''; } diff --git a/packages/kg-default-nodes/test/nodes/call-to-action.test.js b/packages/kg-default-nodes/test/nodes/call-to-action.test.js index 706a7ccd49..296bf3fb3a 100644 --- a/packages/kg-default-nodes/test/nodes/call-to-action.test.js +++ b/packages/kg-default-nodes/test/nodes/call-to-action.test.js @@ -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' }; @@ -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); @@ -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'); diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx index e0b7484cd5..28aca43c1a 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.jsx @@ -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 diff --git a/packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx index cb2555d8fb..24ac1745ca 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx @@ -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); diff --git a/packages/koenig-lexical/src/index.js b/packages/koenig-lexical/src/index.js index 7d948a8494..407dcdbd45 100644 --- a/packages/koenig-lexical/src/index.js +++ b/packages/koenig-lexical/src/index.js @@ -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'; @@ -65,6 +66,7 @@ export { AudioPlugin, CalloutPlugin, + CallToActionPlugin, CardMenuPlugin, CollectionPlugin, DragDropPastePlugin, diff --git a/packages/koenig-lexical/src/nodes/CallToActionNode.jsx b/packages/koenig-lexical/src/nodes/CallToActionNode.jsx new file mode 100644 index 0000000000..188dd69e26 --- /dev/null +++ b/packages/koenig-lexical/src/nodes/CallToActionNode.jsx @@ -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 || '

Hey {first_name, "there"},

'); + } + } + + decorate() { + return ( + + + + ); + } +} + +export function $createCallToActionNode(dataset) { + return new CallToActionNode(dataset); +} + +export function $isCallToActionNode(node) { + return node instanceof CallToActionNode; +} diff --git a/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx b/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx new file mode 100644 index 0000000000..1814cf518e --- /dev/null +++ b/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx @@ -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 ( + <> + {}} + 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={() => {}} + /> + + setShowSnippetToolbar(false)} /> + + + + + + + setShowSnippetToolbar(true)} + /> + + + + ); +}; diff --git a/packages/koenig-lexical/src/nodes/DefaultNodes.js b/packages/koenig-lexical/src/nodes/DefaultNodes.js index dea0ba80a8..46985baff3 100644 --- a/packages/koenig-lexical/src/nodes/DefaultNodes.js +++ b/packages/koenig-lexical/src/nodes/DefaultNodes.js @@ -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'; @@ -60,6 +61,7 @@ const DEFAULT_NODES = [ HtmlNode, FileNode, ButtonNode, + CallToActionNode, ToggleNode, HeaderNode, BookmarkNode, diff --git a/packages/koenig-lexical/src/plugins/AllDefaultPlugins.jsx b/packages/koenig-lexical/src/plugins/AllDefaultPlugins.jsx index 3ee30c2515..403b5bd5fb 100644 --- a/packages/koenig-lexical/src/plugins/AllDefaultPlugins.jsx +++ b/packages/koenig-lexical/src/plugins/AllDefaultPlugins.jsx @@ -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'; @@ -63,6 +64,7 @@ export const AllDefaultPlugins = () => { + ); }; diff --git a/packages/koenig-lexical/src/plugins/CallToActionPlugin.jsx b/packages/koenig-lexical/src/plugins/CallToActionPlugin.jsx new file mode 100644 index 0000000000..e6ab75a1f0 --- /dev/null +++ b/packages/koenig-lexical/src/plugins/CallToActionPlugin.jsx @@ -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; diff --git a/packages/koenig-lexical/test/e2e/cards/cta-card.test.js b/packages/koenig-lexical/test/e2e/cards/cta-card.test.js new file mode 100644 index 0000000000..989ffda0cc --- /dev/null +++ b/packages/koenig-lexical/test/e2e/cards/cta-card.test.js @@ -0,0 +1,31 @@ +import {assertHTML, focusEditor, html, initialize, insertCard} from '../../utils/e2e'; +import {test} from '@playwright/test'; + +test.describe('Call To Action Card', async () => { + let page; + + test.beforeAll(async ({browser}) => { + page = await browser.newPage(); + }); + + test.beforeEach(async () => { + await initialize({page}); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('renders CTA Card', async function () { + await focusEditor(page); + await insertCard(page, {cardName: 'call-to-action'}); + + await assertHTML(page, html` +
+
+
+
+


+ `, {ignoreCardContents: true}); + }); +});