diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 3ad4aae7a0..af3524b998 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -95,9 +95,7 @@ @placeholder={{@bodyPlaceholder}} @cardConfig={{@cardOptions}} @onChange={{@onBodyChange}} - @updateSecondaryInstanceModel={{@updateSecondaryInstanceModel}} @registerAPI={{this.registerEditorAPI}} - @registerSecondaryAPI={{this.registerSecondaryEditorAPI}} @cursorDidExitAtTop={{if this.feature.editorExcerpt this.focusExcerpt this.focusTitle}} @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.js b/ghost/admin/app/components/gh-koenig-editor-lexical.js index be28f73a5f..2319ae9c8a 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.js +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.js @@ -15,7 +15,6 @@ export default class GhKoenigEditorLexical extends Component { uploadUrl = `${ghostPaths().apiRoot}/images/upload/`; editorAPI = null; - secondaryEditorAPI = null; skipFocusEditor = false; @tracked titleIsHovered = false; @@ -233,12 +232,6 @@ export default class GhKoenigEditorLexical extends Component { this.args.registerAPI(API); } - @action - registerSecondaryEditorAPI(API) { - this.secondaryEditorAPI = API; - this.args.registerSecondaryAPI(API); - } - // focus the editor when the editor canvas is clicked below the editor content, // otherwise the browser will defocus the editor and the cursor will disappear @action diff --git a/ghost/admin/app/components/gh-post-settings-menu.hbs b/ghost/admin/app/components/gh-post-settings-menu.hbs index ee91b2af80..1d01dcc5bc 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.hbs +++ b/ghost/admin/app/components/gh-post-settings-menu.hbs @@ -853,7 +853,6 @@ post=this.post editorAPI=this.editorAPI toggleSettingsMenu=this.toggleSettingsMenu - secondaryEditorAPI=this.secondaryEditorAPI }} @close={{this.closePostHistory}} @modifier="total-overlay post-history" /> diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index f2a471a463..ad1c769da6 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -678,43 +678,34 @@ export default class KoenigLexicalEditor extends Component { const multiplayerDocId = cardConfig.post.id; const multiplayerUsername = this.session.user.name; - const KGEditorComponent = ({isInitInstance}) => { - return ( -
- - - {} : this.args.updateWordCount} /> - {} : this.args.updatePostTkCount} /> - -
- ); - }; - return (
Loading editor...

}> - - + + + + +
diff --git a/ghost/admin/app/components/modal-post-history.hbs b/ghost/admin/app/components/modal-post-history.hbs index f7f8ab1261..3989c29db5 100644 --- a/ghost/admin/app/components/modal-post-history.hbs +++ b/ghost/admin/app/components/modal-post-history.hbs @@ -33,7 +33,6 @@ @lexical={{this.selectedRevision.lexical}} @cardConfig={{this.cardConfig}} @registerAPI={{this.registerSelectedEditorApi}} - @registerSecondaryAPI={{this.registerSecondarySelectedEditorApi}} /> diff --git a/ghost/admin/app/components/modal-post-history.js b/ghost/admin/app/components/modal-post-history.js index 4dfa987e2e..05aba6646e 100644 --- a/ghost/admin/app/components/modal-post-history.js +++ b/ghost/admin/app/components/modal-post-history.js @@ -31,7 +31,6 @@ export default class ModalPostHistory extends Component { super(...arguments); this.post = this.args.model.post; this.editorAPI = this.args.model.editorAPI; - this.secondaryEditorAPI = this.args.model.secondaryEditorAPI; this.toggleSettingsMenu = this.args.model.toggleSettingsMenu; } @@ -102,11 +101,6 @@ export default class ModalPostHistory extends Component { this.selectedEditor = api; } - @action - registerSecondarySelectedEditorApi(api) { - this.secondarySelectedEditor = api; - } - @action registerComparisonEditorApi(api) { this.comparisonEditor = api; @@ -136,7 +130,6 @@ export default class ModalPostHistory extends Component { updateEditor: () => { const state = this.editorAPI.editorInstance.parseEditorState(revision.lexical); this.editorAPI.editorInstance.setEditorState(state); - this.secondaryEditorAPI.editorInstance.setEditorState(state); }, closePostHistoryModal: () => { this.closeModal(); diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 21c157d102..71c6331437 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -297,11 +297,6 @@ export default class LexicalEditorController extends Controller { this._timedSaveTask.perform(); } - @action - updateSecondaryInstanceModel(lexical) { - this.set('post.secondaryLexicalState', JSON.stringify(lexical)); - } - @action updateTitleScratch(title) { this.set('post.titleScratch', title); @@ -428,11 +423,6 @@ export default class LexicalEditorController extends Controller { this.editorAPI = API; } - @action - registerSecondaryEditorAPI(API) { - this.secondaryEditorAPI = API; - } - @action clearFeatureImage() { this.post.set('featureImage', null); @@ -1239,7 +1229,8 @@ export default class LexicalEditorController extends Controller { return false; } - // If the Adapter failed to save the post, isError will be true, and we should consider the post still dirty. + // if the Adapter failed to save the post isError will be true + // and we should consider the post still dirty. if (post.get('isError')) { this._leaveModalReason = {reason: 'isError', context: post.errors.messages}; return true; @@ -1254,58 +1245,53 @@ export default class LexicalEditorController extends Controller { return true; } - // Title scratch comparison + // titleScratch isn't an attr so needs a manual dirty check if (post.titleScratch !== post.title) { this._leaveModalReason = {reason: 'title is different', context: {current: post.title, scratch: post.titleScratch}}; return true; } - // Lexical and scratch comparison + // scratch isn't an attr so needs a manual dirty check let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); - let secondaryLexical = post.get('secondaryLexicalState'); + // additional guard in case we are trying to compare null with undefined + if (scratch || lexical) { + if (scratch !== lexical) { + // lexical can dynamically set direction on loading editor state (e.g. "rtl"/"ltr") per the DOM context + // and we need to ignore this as a change from the user; see https://github.com/facebook/lexical/issues/4998 + const scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; + const lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; - let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; - let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; - let secondaryLexicalChildNodes = secondaryLexical ? JSON.parse(secondaryLexical).root?.children : []; + // // nullling is typically faster than delete + scratchChildNodes.forEach(child => child.direction = null); + lexicalChildNodes.forEach(child => child.direction = null); - lexicalChildNodes.forEach(child => child.direction = null); - scratchChildNodes.forEach(child => child.direction = null); - secondaryLexicalChildNodes.forEach(child => child.direction = null); + if (JSON.stringify(scratchChildNodes) === JSON.stringify(lexicalChildNodes)) { + return false; + } - // Compare initLexical with scratch - let isSecondaryDirty = secondaryLexical && scratch && JSON.stringify(secondaryLexicalChildNodes) !== JSON.stringify(scratchChildNodes); - - // Compare lexical with scratch - let isLexicalDirty = lexical && scratch && JSON.stringify(lexicalChildNodes) !== JSON.stringify(scratchChildNodes); - - // If either comparison is not dirty, return false, because scratch is always up to date. - if (!isSecondaryDirty || !isLexicalDirty) { - return false; + this._leaveModalReason = {reason: 'lexical is different', context: {current: lexical, scratch}}; + return true; + } } - // If both comparisons are dirty, consider the post dirty - if (isSecondaryDirty && isLexicalDirty) { - this._leaveModalReason = {reason: 'initLexical and lexical are different from scratch', context: {secondaryLexical, lexical, scratch}}; - return true; - } - - // New+unsaved posts always return `hasDirtyAttributes: true` + // new+unsaved posts always return `hasDirtyAttributes: true` // so we need a manual check to see if any if (post.get('isNew')) { - let changedAttributes = Object.keys(post.changedAttributes() || {}); + let changedAttributes = Object.keys(post.changedAttributes()); + if (changedAttributes.length) { this._leaveModalReason = {reason: 'post.changedAttributes.length > 0', context: post.changedAttributes()}; } return changedAttributes.length ? true : false; } - // We've covered all the non-tracked cases we care about so fall + // we've covered all the non-tracked cases we care about so fall // back on Ember Data's default dirty attribute checks let {hasDirtyAttributes} = post; + if (hasDirtyAttributes) { this._leaveModalReason = {reason: 'post.hasDirtyAttributes === true', context: post.changedAttributes()}; - return true; } return hasDirtyAttributes; diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 835d24d0a2..1ffb06d8d0 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -136,9 +136,6 @@ export default Model.extend(Comparable, ValidationEngine, { scratch: null, lexicalScratch: null, titleScratch: null, - //This is used to store the initial lexical state from the - // secondary editor to get the schema up to date in case its outdated - secondaryLexicalState: null, // For use by date/time pickers - will be validated then converted to UTC // on save. Updated by an observer whenever publishedAtUTC changes. diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index b7ee9a4a74..bd9d5d51e7 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -73,7 +73,6 @@ @body={{readonly this.post.lexicalScratch}} @bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}} @onBodyChange={{this.updateScratch}} - @updateSecondaryInstanceModel={{this.updateSecondaryInstanceModel}} @headerOffset={{editor.headerHeight}} @scrollContainerSelector=".gh-koenig-editor" @scrollOffsetBottomSelector=".gh-mobile-nav-bar" @@ -98,7 +97,6 @@ }} @postType={{this.post.displayName}} @registerAPI={{this.registerEditorAPI}} - @registerSecondaryAPI={{this.registerSecondaryEditorAPI}} @savePostTask={{this.savePostTask}} /> @@ -138,7 +136,6 @@ @updateSlugTask={{this.updateSlugTask}} @savePostTask={{this.savePostTask}} @editorAPI={{this.editorAPI}} - @secondaryEditorAPI={{this.secondaryEditorAPI}} @toggleSettingsMenu={{this.toggleSettingsMenu}} /> {{/if}} diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 5c89d544e2..088f22391d 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -195,60 +195,6 @@ describe('Unit: Controller: lexical-editor', function () { }); describe('hasDirtyAttributes', function () { - it('detects new post with changed attributes as dirty (autosave)', async function () { - const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content updated","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - isNew: true, - title: '', - titleScratch: '', - status: 'draft', - lexical: initialLexicalString, - lexicalScratch: lexicalScratch, - secondaryLexicalState: initialLexicalString - })); - - let isDirty = controller.get('hasDirtyAttributes'); - expect(isDirty).to.be.true; - }); - - it('does not detect new post as dirty when there are no changes', async function () { - const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - isNew: true, - title: '', - titleScratch: '', - status: 'draft', - lexical: initialLexicalString, - lexicalScratch: initialLexicalString, - secondaryLexicalState: initialLexicalString - })); - - let isDirty = controller.get('hasDirtyAttributes'); - expect(isDirty).to.be.false; - }); - - it('marks isNew post as dirty when lexicalScratch differs from lexical and secondaryLexical', async function () { - const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content scratch","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - isNew: true, - title: '', - titleScratch: '', - status: 'draft', - lexical: initialLexicalString, - lexicalScratch: lexicalScratch, - secondaryLexicalState: initialLexicalString, - changedAttributes: () => ({title: ['', 'New Title']}) - })); - - let isDirty = controller.get('hasDirtyAttributes'); - expect(isDirty).to.be.true; - }); - it('Changes in the direction field in the lexical string are not considered dirty', async function () { let controller = this.owner.lookup('controller:lexical-editor'); @@ -262,8 +208,7 @@ describe('Unit: Controller: lexical-editor', function () { titleScratch: 'this is a title', status: 'published', lexical: initialLexicalString, - lexicalScratch: initialLexicalString, - secondaryLexicalState: initialLexicalString + lexicalScratch: initialLexicalString })); // synthetically update the lexicalScratch as if the editor itself made the modifications on loading the initial editorState @@ -280,104 +225,5 @@ describe('Unit: Controller: lexical-editor', function () { isDirty = controller.get('hasDirtyAttributes'); expect(isDirty).to.be.true; }); - - it('dirty is false if secondaryLexical and scratch matches, but lexical is outdated', async function () { - const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const secondLexicalInstance = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Here's some new text","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - title: 'this is a title', - titleScratch: 'this is a title', - status: 'published', - lexical: initialLexicalString, - lexicalScratch: lexicalScratch, - secondaryLexicalState: secondLexicalInstance - })); - - let isDirty = controller.get('hasDirtyAttributes'); - - expect(isDirty).to.be.false; - }); - - it('dirty is true if secondaryLexical and lexical does not match scratch', async function () { - const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content1234","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - const secondLexicalInstance = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Here's some new text","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`; - - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - title: 'this is a title', - titleScratch: 'this is a title', - status: 'published', - lexical: initialLexicalString, - lexicalScratch: lexicalScratch, - secondaryLexicalState: secondLexicalInstance - })); - - controller.send('updateScratch',JSON.parse(lexicalScratch)); - - let isDirty = controller.get('hasDirtyAttributes'); - - expect(isDirty).to.be.true; - }); - - it('dirty is false if no Post', async function () { - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', null); - - let isDirty = controller.get('hasDirtyAttributes'); - - expect(isDirty).to.be.false; - }); - - it('returns true if current tags differ from previous tags', async function () { - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - tags: [{name: 'test'}], - tagsString: 'test', - changedAttributes: () => ({tags: [[{name: 'test'}], [{name: 'changed'}]]}) - })); - - let isDirty = controller.get('hasDirtyAttributes'); - - expect(isDirty).to.be.true; - }); - - it('returns false when the post is new but has no changed attributes', async function () { - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - isNew: true, - title: '', - titleScratch: '', - status: 'draft', - lexical: '', - lexicalScratch: '', - secondaryLexicalState: '', - changedAttributes: () => ({}) // no changes - })); - - let isDirty = controller.get('hasDirtyAttributes'); - expect(isDirty).to.be.false; - }); - - it('skips new post check if post is not new', async function () { - let controller = this.owner.lookup('controller:lexical-editor'); - controller.set('post', EmberObject.create({ - isNew: false, - title: 'Sample Title', - titleScratch: 'Sample Title', - status: 'draft', - lexical: '', - lexicalScratch: '', - secondaryLexicalState: '', - changedAttributes: () => ({}) - })); - - let isDirty = controller.get('hasDirtyAttributes'); - // The test passes if no errors occur and it doesn't return true for new post condition - expect(isDirty).to.be.false; - }); }); }); diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index 788a08c2c2..013f62ca17 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -298,12 +298,11 @@ test.describe('Publishing', () => { test('Renders secondary hidden lexical editor', async ({sharedPage: adminPage}) => { await adminPage.goto('/ghost'); + await createPostDraft(adminPage, {title: 'Secondary lexical editor test', body: 'This is my post body.'}); - const secondaryLexicalEditor = adminPage.locator('[data-secondary-instance="true"]'); - // Check if the secondary lexical editor exists - await expect(secondaryLexicalEditor).toHaveCount(1); - // Check if it is hidden - await expect(secondaryLexicalEditor).toBeHidden(); + + // Check if the secondary lexical editor exists but is hidden. + expect(await adminPage.locator('[data-secondary-instance="true"]')).toBeHidden(); }); });