Skip to content

Commit

Permalink
Added editable Sponsored label
Browse files Browse the repository at this point in the history
ref https://linear.app/ghost/issue/PLG-325/turn-sponsored-into-editable-text

- Added a new node that would allow us to add customise the the
  Sponsored Label
  • Loading branch information
ronaldlangeveld committed Feb 6, 2025
1 parent a8157e9 commit 206426d
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class CallToActionNode extends generateDecoratorNode({
{name: 'buttonColor', default: ''},
{name: 'buttonTextColor', default: ''},
{name: 'hasSponsorLabel', default: true},
{name: 'sponsorLabel', default: '<p>Sponsored</p>'},
{name: 'sponsorLabel', default: '<p><span style="white-space: pre-wrap;">SPONSORED</span></p>'},
{name: 'backgroundColor', default: 'grey'},
{name: 'hasImage', default: false},
{name: 'imageUrl', default: ''}
Expand Down
8 changes: 4 additions & 4 deletions packages/kg-default-nodes/test/nodes/call-to-action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('CallToActionNode', function () {
dataset = {
layout: 'minimal',
textValue: 'This is a cool advertisement',
sponsorLabel: '<p>Sponsored</p>',
sponsorLabel: '<p><span style="white-space: pre-wrap;">SPONSORED</span></p>',
showButton: true,
buttonText: 'click me',
buttonUrl: 'http://blog.com/post1',
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('CallToActionNode', function () {
callToActionNode.buttonUrl = 'http://blog.com/post1';
callToActionNode.buttonUrl.should.equal('http://blog.com/post1');

callToActionNode.sponsorLabel.should.equal('<p>Sponsored</p>');
callToActionNode.sponsorLabel.should.equal('<p><span style="white-space: pre-wrap;">SPONSORED</span></p>');
callToActionNode.sponsorLabel = 'This post is brought to you by our sponsors';
callToActionNode.sponsorLabel.should.equal('This post is brought to you by our sponsors');

Expand Down Expand Up @@ -222,7 +222,7 @@ describe('CallToActionNode', function () {
buttonUrl: 'http://someblog.com/somepost',
hasImage: true,
hasSponsorLabel: true,
sponsorLabel: '<p>Sponsored</p>',
sponsorLabel: '<p><span style="white-space: pre-wrap;">SPONSORED</span></p>',
imageUrl: '/content/images/2022/11/koenig-lexical.jpg',
layout: 'minimal',
showButton: true,
Expand All @@ -237,7 +237,7 @@ describe('CallToActionNode', function () {
html.should.containEql('background-color: #F0F0F0');
html.should.containEql('Get access now');
html.should.containEql('http://someblog.com/somepost');
html.should.containEql('Sponsored'); // because hasSponsorLabel is true
html.should.containEql('<p><span style="white-space: pre-wrap;">SPONSORED</span></p>'); // because hasSponsorLabel is true
html.should.containEql('/content/images/2022/11/koenig-lexical.jpg'); // because hasImage is true
html.should.containEql('This is a new CTA Card via email.');
}));
Expand Down
20 changes: 17 additions & 3 deletions packages/koenig-lexical/src/components/ui/cards/CtaCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export function CtaCard({
hasSponsorLabel,
htmlEditor,
htmlEditorInitialState,
sponsorLabelHtmlEditor,
sponsorLabelHtmlEditorInitialState,
imageSrc,
isEditing,
layout,
Expand Down Expand Up @@ -225,13 +227,25 @@ export function CtaCard({
'pb-3': color === 'none' && hasSponsorLabel
}
)} data-cta-layout={layout}>

{/* Sponsor label */}
{hasSponsorLabel && (
<div className={clsx(
'not-kg-prose py-3',
{'mx-6': color !== 'none'}
'mx-6 py-3',
)}>
<p className="font-sans text-2xs font-semibold uppercase leading-8 tracking-normal text-grey-900/40 dark:text-grey-100/40" data-testid="sponsor-label">Sponsored</p>
<KoenigNestedEditor
autoFocus={true}
dataTestId={'sponsor-label-editor'}
hasSettingsPanel={true}
initialEditor={sponsorLabelHtmlEditor}
initialEditorState={sponsorLabelHtmlEditorInitialState}
nodes='basic'
textClassName={clsx(
'kg-prose w-full whitespace-normal bg-transparent font-sans !text-2xs text-grey-900/40 dark:text-grey-100/40'
)}
>
<ReplacementStringsPlugin />
</KoenigNestedEditor>
</div>
)}

Expand Down
18 changes: 18 additions & 0 deletions packages/koenig-lexical/src/nodes/CallToActionNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const INSERT_CTA_COMMAND = createCommand();
export class CallToActionNode extends BaseCallToActionNode {
__ctaHtmlEditor;
__ctaHtmlEditorInitialState;
__sponsorLabelHtmlEditor;
__sponsorLabelHtmlEditorInitialState;

static kgMenu = {
label: 'Call to Action',
Expand Down Expand Up @@ -40,11 +42,15 @@ export class CallToActionNode extends BaseCallToActionNode {

// set up nested editor instances
setupNestedEditor(this, '__ctaHtmlEditor', {editor: dataset.ctaHtmlEditor, nodes: BASIC_NODES});
setupNestedEditor(this, '__sponsorLabelHtmlEditor', {editor: dataset.sponsorLabelHtmlEditor, nodes: BASIC_NODES});

// populate nested editors on initial construction
if (!dataset.ctaHtmlEditor && dataset.textValue) {
populateNestedEditor(this, '__ctaHtmlEditor', `${dataset.textValue}`); // we serialize with no wrapper
}
if (!dataset.sponsorLabelHtmlEditor) {
populateNestedEditor(this, '__sponsorLabelHtmlEditor', `${dataset.sponsorLabel || '<p><span style="white-space: pre-wrap;">SPONSORED</span></p>'}`);
}
}

getDataset() {
Expand All @@ -53,6 +59,8 @@ export class CallToActionNode extends BaseCallToActionNode {
const self = this.getLatest();
dataset.ctaHtmlEditor = self.__ctaHtmlEditor;
dataset.ctaHtmlEditorInitialState = self.__ctaHtmlEditorInitialState;
dataset.sponsorLabelHtmlEditor = self.__sponsorLabelHtmlEditor;
dataset.sponsorLabelHtmlEditorInitialState = self.__sponsorLabelHtmlEditorInitialState;

return dataset;
}
Expand All @@ -70,6 +78,14 @@ export class CallToActionNode extends BaseCallToActionNode {
});
}

if (this.__sponsorLabelHtmlEditor) {
this.__sponsorLabelHtmlEditor.getEditorState().read(() => {
const html = $generateHtmlFromNodes(this.__sponsorLabelHtmlEditor, null);
const cleanedHtml = cleanBasicHtml(html, {allowBr: false});
json.sponsorLabel = cleanedHtml;
});
}

return json;
}

Expand All @@ -93,6 +109,8 @@ export class CallToActionNode extends BaseCallToActionNode {
layout={this.layout}
nodeKey={this.getKey()}
showButton={this.showButton}
sponsorLabelHtmlEditor={this.__sponsorLabelHtmlEditor}
sponsorLabelHtmlEditorInitialState={this.__sponsorLabelHtmlEditorInitialState}
textValue={this.textValue}
/>
</KoenigCardWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const CallToActionNodeComponent = ({
buttonColor,
htmlEditor,
htmlEditorInitialState,
buttonTextColor
buttonTextColor,
sponsorLabelHtmlEditor,
sponsorLabelHtmlEditorInitialState
}) => {
const [editor] = useLexicalComposerContext();
const {isEditing, isSelected, setEditing} = React.useContext(CardContext);
Expand Down Expand Up @@ -111,7 +113,7 @@ export const CallToActionNodeComponent = ({

React.useEffect(() => {
htmlEditor.setEditable(isEditing);
}, [isEditing, htmlEditor]);
}, [isEditing, htmlEditor, sponsorLabelHtmlEditor]);

return (
<>
Expand All @@ -135,6 +137,8 @@ export const CallToActionNodeComponent = ({
setEditing={setEditing}
setFileInputRef={ref => fileInputRef.current = ref}
showButton={showButton}
sponsorLabelHtmlEditor={sponsorLabelHtmlEditor}
sponsorLabelHtmlEditorInitialState={sponsorLabelHtmlEditorInitialState}
text={textValue}
updateButtonText={handleButtonTextChange}
updateButtonUrl={handleButtonUrlChange}
Expand Down
51 changes: 48 additions & 3 deletions packages/koenig-lexical/test/e2e/cards/cta-card.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ test.describe('Call To Action Card', async () => {
buttonUrl: 'http://someblog.com/somepost',
hasImage: true,
hasSponsorLabel: true,
sponsorLabel: '<p><span style="white-space: pre-wrap;">SPONSORED</span></p>',
imageUrl: '/content/images/2022/11/koenig-lexical.jpg',
layout: 'minimal',
showButton: true,
Expand Down Expand Up @@ -264,10 +265,54 @@ test.describe('Call To Action Card', async () => {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
await page.click('[data-testid="sponsor-label-toggle"]');
expect(await page.isVisible('[data-testid="sponsor-label"]')).toBe(false);

expect(await page.isVisible('[data-testid="sponsor-label-editor"]')).toBe(false);
await page.click('[data-testid="sponsor-label-toggle"]');
expect(await page.isVisible('[data-testid="sponsor-label"]')).toBe(true);
expect(await page.isVisible('[data-testid="sponsor-label-editor"]')).toBe(true);
});

test('sponsor label is active by default', async function () {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
const sponsorLabel = await page.locator('[data-testid="sponsor-label-editor"]');
await expect(sponsorLabel).toBeVisible();
});

test('sponsor label text is SPONSORED by default', async function () {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
const sponsorLabel = await page.locator('[data-testid="sponsor-label-editor"]');
await expect(sponsorLabel).toContainText('SPONSORED');
});

test('can modify sponsor label text', async function () {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
const sponsorEditor = await page.locator('[data-testid="sponsor-label-editor"]');
await page.click('[data-testid="sponsor-label-editor"]');
// clear the default text by hitting backspace 9 times
for (let i = 0; i < 9; i++) {
await page.keyboard.press('Backspace');
}
await expect(sponsorEditor).toContainText('');
await page.keyboard.type('Sponsored by Ghost');
const content = page.locator('[data-testid="sponsor-label-editor"]');
await expect(content).toContainText('Sponsored by Ghost');
});

test('content editor placeholder is visible', async function () {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
const contentEditor = await page.locator('[data-testid="cta-card-content-editor"]');
await expect(contentEditor).toContainText('Write something worth clicking...');
});

test('can modify content editor text', async function () {
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
await page.click('[data-testid="cta-card-content-editor"]');
await page.keyboard.type('This is a new CTA Card.');
const content = page.locator('[data-testid="cta-card-content-editor"]');
await expect(content).toContainText('This is a new CTA Card.');
});

test('can change background colours', async function () {
Expand Down

0 comments on commit 206426d

Please sign in to comment.