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
30 changes: 29 additions & 1 deletion ghost/admin/app/components/gh-editor-feature-image.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ function hasParagraphWrapper(html) {
return doc.body?.firstElementChild?.tagName === 'P';
}

function cleanCaptionHtml(html) {
return cleanBasicHtml(html || '', {firstChildInnerContent: true});
}

function isLexicalPlainTextSpan(element) {
return element.tagName === 'SPAN' && element.style.length === 1 && element.style.whiteSpace === 'pre-wrap';
}

function normalizeCaptionHtml(html) {
Comment thread
aileen marked this conversation as resolved.
// Lexical wraps plain text in spans with `white-space: pre-wrap` on load.
// Ignore those wrappers so API-loaded captions do not mark the post as unsaved.
const cleanedHtml = cleanCaptionHtml(html);
const domParser = new DOMParser();
const doc = domParser.parseFromString(cleanedHtml, 'text/html');

doc.body.querySelectorAll('span').forEach((element) => {
if (isLexicalPlainTextSpan(element)) {
element.replaceWith(...element.childNodes);
}
});

return doc.body.innerHTML.trim();
}

export default class GhEditorFeatureImageComponent extends Component {
@service settings;

Expand All @@ -31,7 +55,11 @@ export default class GhEditorFeatureImageComponent extends Component {

@action
setCaption(html) {
const cleanedHtml = cleanBasicHtml(html || '', {firstChildInnerContent: true});
const cleanedHtml = cleanCaptionHtml(html);
if (normalizeCaptionHtml(cleanedHtml) === normalizeCaptionHtml(this.caption)) {
return;
}

this.args.updateCaption(cleanedHtml);
}

Expand Down
15 changes: 14 additions & 1 deletion ghost/admin/tests/acceptance/editor/feature-image-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ describe('Acceptance: Feature Image', function () {
expect(await find('.gh-editor-feature-image-caption').textContent).to.contain('Hello dogggos');
});

it('does not enable update button when a feature image caption is loaded from the API', async function () {
const post = this.server.create('post', {
status: 'published',
featureImage: 'https://static.ghost.org/v4.0.0/images/feature-image.jpg',
featureImageCaption: 'Feature image caption from the API'
});

await visit(`/editor/post/${post.id}`);

expect(find('.gh-editor-feature-image-caption')).to.have.rendered.text('Feature image caption from the API');
expect(find('[data-test-button="publish-save"]').disabled).to.be.true;
});

it('does not attempt to save if already deleted and goes back to posts', async function () {
// avoids an infinite loop when the post is deleted and the save button is clicked, potential race condition
const post = this.server.create('post', {status: 'published', featureImage: 'https://static.ghost.org/v4.0.0/images/feature-image.jpg', featureImageCaption: '<span style="white-space: pre-wrap;">Hello dogggos</span>'});
Expand All @@ -34,4 +47,4 @@ describe('Acceptance: Feature Image', function () {

expect(currentURL()).to.equal('/posts');
});
});
});
Loading