Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/floppy-bobcats-spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/kg-default-nodes": patch
---

Fixed header cards losing their layout when imported from HTML.
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
import type {LexicalNode} from 'lexical';

/**
* Reverses the class names applied by the v2 renderer's `getCardClasses`.
*
* The layout is encoded in the card's classes, so inferring it from the presence of an image alone collapses
* every regular/wide/full card into split when rendered HTML is imported again (e.g. via the Admin API with
* `?source=html`).
*/
function getHeaderV2Layout(div: HTMLElement): string {
// Split has to be checked first β€” split cards also carry `kg-width-full`.
if (div.classList.contains('kg-layout-split')) {
return 'split';
}
if (div.classList.contains('kg-width-full')) {
return 'full';
}
if (div.classList.contains('kg-width-wide')) {
return 'wide';
}
if (div.classList.contains('kg-width-regular')) {
return 'regular';
}

// Hand-authored HTML may omit the width classes. The renderer only nests the image inside the content
// wrapper for split layouts, so the structure is the next best signal.
const image = div.querySelector('.kg-header-card-image');
const content = div.querySelector('.kg-header-card-content');
if (image && content?.contains(image)) {
return 'split';
}

return 'full';
}

export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>) => LexicalNode) {
return {
div: (nodeElem: HTMLElement) => {
Expand Down Expand Up @@ -53,7 +86,7 @@ export function parseHeaderNode(HeaderNode: new (data: Record<string, unknown>)
const buttonElement = div.querySelector('.kg-header-card-button');
const alignment = div.classList.contains('kg-align-center') ? 'center' : '';
const backgroundImageSrc = div.querySelector('.kg-header-card-image')?.getAttribute('src');
const layout = backgroundImageSrc ? 'split' : '';
const layout = getHeaderV2Layout(div);
const backgroundColor = div.classList.contains('kg-style-accent') ? 'accent' : div.getAttribute('data-background-color');
const buttonColor = buttonElement?.getAttribute('data-button-color') || '';
const textColor = headerElement?.getAttribute('data-text-color') || '';
Expand Down
79 changes: 78 additions & 1 deletion koenig/kg-default-nodes/test/nodes/header.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ describe('HeaderNode', function () {
expect(node.buttonColor).toBe('#abcdef');
expect(node.alignment).toBe('center');
expect(node.backgroundImageSrc).toBe('https://example.com/image.jpg');
expect(node.layout).toBe('split');
expect(node.layout).toBe('full');
expect(node.textColor).toBe('#abcdef');
expect(node.header).toBe('Header');
expect(node.subheader).toBe('Subheader');
Expand All @@ -341,6 +341,83 @@ describe('HeaderNode', function () {
expect(node.buttonTextColor).toBe('#abcdef');
}));

it('parses the layout from the card classes', editorTest(function () {
const cards: [string, string][] = [
['kg-width-regular', 'regular'],
['kg-width-wide', 'wide'],
['kg-width-full kg-content-wide', 'full'],
['kg-layout-split kg-width-full', 'split']
];

cards.forEach(([classes, expected]) => {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 ${classes}" data-background-color="#000000">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content">
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Header</h2>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document) as HeaderNode[];
expect(nodes.length).toBe(1);
expect(nodes[0].layout).toBe(expected);
});
}));

it('round-trips every layout with and without a background image', editorTest(function () {
const layouts = ['regular', 'wide', 'full', 'split'];
const backgroundImages = ['', 'https://example.com/image.jpg'];

layouts.forEach((layout) => {
backgroundImages.forEach((backgroundImageSrc) => {
const headerNode = $createHeaderNode({...dataset, layout, backgroundImageSrc});
const {element} = headerNode.exportDOM(editor, exportOptions);
const document = createDocument((element as HTMLElement).outerHTML);
const nodes = $generateNodesFromDOM(editor, document) as HeaderNode[];

expect(nodes.length).toBe(1);
expect(nodes[0].layout).toBe(layout);
expect(nodes[0].backgroundImageSrc).toBe(backgroundImageSrc);
});
});
}));

it('does not force split layout when a full-width card has a background image', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2 kg-width-full kg-content-wide" data-background-color="#000000">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-content">
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading" data-text-color="#FFFFFF">Title</h2>
<p class="kg-header-card-subheading" data-text-color="#FFFFFF">Subtitle</p>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document) as HeaderNode[];
expect(nodes.length).toBe(1);
expect(nodes[0].layout).toBe('full');
expect(nodes[0].backgroundImageSrc).toBe('https://example.com/image.jpg');
}));

it('falls back to the image position when the layout classes are missing', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-v2" data-background-color="#000000">
<div class="kg-header-card-content">
<picture><img class="kg-header-card-image" src="https://example.com/image.jpg" alt="" /></picture>
<div class="kg-header-card-text kg-align-center">
<h2 class="kg-header-card-heading">Header</h2>
</div>
</div>
</div>`;
const document = createDocument(htmlstring);
const nodes = $generateNodesFromDOM(editor, document) as HeaderNode[];
expect(nodes.length).toBe(1);
expect(nodes[0].layout).toBe('split');
}));

it('does not parse a v1 header as v2', editorTest(function () {
const htmlstring = `
<div class="kg-card kg-header-card kg-size-large kg-style-image" data-kg-background-image="https://example.com/image.jpg" style="background-image: url(https://example.com/image.jpg)">
Expand Down
Loading