diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs
index 3ad4aae7a008..af3524b99898 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 be28f73a5f74..2319ae9c8a6b 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 ee91b2af8091..1d01dcc5bcee 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 f2a471a463b2..ad1c769da60c 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 f7f8ab1261c8..3989c29db590 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 4dfa987e2e70..05aba6646e67 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 2b8342afd9e5..71c633143700 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);
@@ -1231,6 +1221,7 @@ export default class LexicalEditorController extends Controller {
_timedSaveTask;
/* Private methods -------------------------------------------------------*/
+
_hasDirtyAttributes() {
let post = this.post;
@@ -1238,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;
@@ -1253,32 +1245,37 @@ 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');
-
- let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : [];
- let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : [];
- let secondaryLexicalChildNodes = secondaryLexical ? JSON.parse(secondaryLexical).root?.children : [];
-
- lexicalChildNodes.forEach(child => child.direction = null);
- scratchChildNodes.forEach(child => child.direction = null);
- secondaryLexicalChildNodes.forEach(child => child.direction = null);
-
- // Compare initLexical with scratch
- let isSecondaryDirty = secondaryLexical && scratch && JSON.stringify(secondaryLexicalChildNodes) !== JSON.stringify(scratchChildNodes);
+ // 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 : [];
+
+ // // nullling is typically faster than delete
+ scratchChildNodes.forEach(child => child.direction = null);
+ lexicalChildNodes.forEach(child => child.direction = null);
+
+ if (JSON.stringify(scratchChildNodes) === JSON.stringify(lexicalChildNodes)) {
+ return false;
+ }
- // Compare lexical with scratch
- let isLexicalDirty = lexical && scratch && JSON.stringify(lexicalChildNodes) !== JSON.stringify(scratchChildNodes);
+ this._leaveModalReason = {reason: 'lexical is different', context: {current: 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());
@@ -1289,26 +1286,15 @@ export default class LexicalEditorController extends Controller {
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;
}
- // If either comparison is not dirty, return false, because scratch is always up to date.
- if (!isSecondaryDirty || !isLexicalDirty) {
- return false;
- }
-
- // 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;
- }
-
- return false;
+ return hasDirtyAttributes;
}
_showSaveNotification(prevStatus, status, delayed) {
diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js
index 835d24d0a2f4..1ffb06d8d0b9 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 b7ee9a4a747b..bd9d5d51e7b3 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 acf7a5e95b0c..088f22391dd6 100644
--- a/ghost/admin/tests/unit/controllers/editor-test.js
+++ b/ghost/admin/tests/unit/controllers/editor-test.js
@@ -208,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
@@ -226,47 +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;
- });
});
});