From 5ad1bb0cb073931645c98eb158923d7c9360cefe Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 30 Jul 2024 15:44:25 +0800 Subject: [PATCH 01/23] WIP double instance on KG lexical --- .../app/components/koenig-lexical-editor.js | 60 +++++++++++-------- ghost/admin/app/models/post.js | 1 + 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 4365e4e26cc..e2507343647 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -669,34 +669,46 @@ export default class KoenigLexicalEditor extends Component { const multiplayerDocId = cardConfig.post.id; const multiplayerUsername = this.session.user.name; + const KGEditorComponent = ({isInitInstance}) => { + const handleInitInstance = (data) => { + + }; + return ( +
+ + + + + +
+ ); + }; + return (
Loading editor...

}> - - - - - + +
diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 1ffb06d8d0b..e7206b16e0f 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -136,6 +136,7 @@ export default Model.extend(Comparable, ValidationEngine, { scratch: null, lexicalScratch: null, titleScratch: null, + initLexicalScratch: null, // For use by date/time pickers - will be validated then converted to UTC // on save. Updated by an observer whenever publishedAtUTC changes. From 2db2e7aaeede8dffb34a4def4d9e1a882099320e Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 30 Jul 2024 16:54:42 +0800 Subject: [PATCH 02/23] Fixup --- .../app/components/gh-koenig-editor-lexical.hbs | 1 + ghost/admin/app/components/koenig-lexical-editor.js | 5 +++-- ghost/admin/app/controllers/lexical-editor.js | 13 ++++++++++++- ghost/admin/app/templates/lexical-editor.hbs | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index af3524b9989..82513daad76 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -99,6 +99,7 @@ @cursorDidExitAtTop={{if this.feature.editorExcerpt this.focusExcerpt this.focusTitle}} @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} + @initLexicalScratch={{@initLexicalScratch}} /> diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index e2507343647..7ff8c52e901 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -671,7 +671,8 @@ export default class KoenigLexicalEditor extends Component { const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { - + // const newjson = JSON.stringify(data); + this.args.initLexicalScratch(data); }; return (
@@ -693,7 +694,7 @@ export default class KoenigLexicalEditor extends Component { cursorDidExitAtTop={this.args.cursorDidExitAtTop} placeholderText={this.args.placeholder} darkMode={this.feature.nightShift} - onChange={isInitInstance ? handleInitInstance : this.args.onChange} + onChange={isInitInstance ? e => handleInitInstance(e) : this.args.onChange} registerAPI={this.args.registerAPI} /> diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 71c63314370..0ba1d8a6b37 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -43,6 +43,7 @@ const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ 'post.lexicalScratch', + 'post.initLexicalScratch', 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', @@ -404,6 +405,13 @@ export default class LexicalEditorController extends Controller { this.set('postTkCount', count); } + @action + initLexicalScratch(data) { + console.log(data); + this.set('post.initLexicalScratch', JSON.stringify(data)); + // this.set('post.lexicalScratch', JSON.stringify(data)); + } + @action updateFeatureImageTkCount(count) { this.set('featureImageTkCount', count); @@ -1026,6 +1034,7 @@ export default class LexicalEditorController extends Controller { // TODO: can these be `boundOneWay` on the model as per the other attrs? post.set('titleScratch', post.get('title')); post.set('lexicalScratch', post.get('lexical')); + // post.set('initLexicalScratch', post.get('lexical')); this._previousTagNames = this._tagNames; @@ -1252,8 +1261,10 @@ export default class LexicalEditorController extends Controller { } // scratch isn't an attr so needs a manual dirty check - let lexical = post.get('lexical'); + let lexical = post.get('initLexicalScratch'); let scratch = post.get('lexicalScratch'); + + console.log('lexical', lexical); // additional guard in case we are trying to compare null with undefined if (scratch || lexical) { if (scratch !== lexical) { diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index bd9d5d51e7b..723f83d6700 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -79,6 +79,7 @@ @onEditorCreated={{this.setKoenigEditor}} @updateWordCount={{this.updateWordCount}} @updatePostTkCount={{this.updatePostTkCount}} + @initLexicalScratch={{this.initLexicalScratch}} @updateFeatureImageTkCount={{this.updateFeatureImageTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} From 652e92d1f82a9c0daf35ea7c85f8f295f7a10f5e Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 30 Jul 2024 17:51:22 +0800 Subject: [PATCH 03/23] Removed console logs --- ghost/admin/app/controllers/lexical-editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 0ba1d8a6b37..f6f0bb29d1b 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -1264,7 +1264,6 @@ export default class LexicalEditorController extends Controller { let lexical = post.get('initLexicalScratch'); let scratch = post.get('lexicalScratch'); - console.log('lexical', lexical); // additional guard in case we are trying to compare null with undefined if (scratch || lexical) { if (scratch !== lexical) { From 20d3b815bf0b5159c98ecb4ef2f187617c7dc79a Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Wed, 31 Jul 2024 13:39:00 +0700 Subject: [PATCH 04/23] Fixed linting --- ghost/admin/app/components/koenig-lexical-editor.js | 1 - ghost/admin/app/controllers/lexical-editor.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 7ff8c52e901..42a03c70841 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -671,7 +671,6 @@ export default class KoenigLexicalEditor extends Component { const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { - // const newjson = JSON.stringify(data); this.args.initLexicalScratch(data); }; return ( diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index f6f0bb29d1b..4b06c11a889 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -407,9 +407,7 @@ export default class LexicalEditorController extends Controller { @action initLexicalScratch(data) { - console.log(data); this.set('post.initLexicalScratch', JSON.stringify(data)); - // this.set('post.lexicalScratch', JSON.stringify(data)); } @action From 0fb95e176927e74953b143208960bf3da81ab1fe Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 1 Aug 2024 08:30:17 +0700 Subject: [PATCH 05/23] Added dirty check --- .../admin/app/components/gh-koenig-editor-lexical.hbs | 2 +- ghost/admin/app/components/koenig-lexical-editor.js | 7 ++++++- ghost/admin/app/controllers/lexical-editor.js | 10 +++++----- ghost/admin/app/models/post.js | 1 - ghost/admin/app/templates/lexical-editor.hbs | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 82513daad76..488335edf87 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -99,7 +99,7 @@ @cursorDidExitAtTop={{if this.feature.editorExcerpt this.focusExcerpt this.focusTitle}} @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} - @initLexicalScratch={{@initLexicalScratch}} + @initLexical={{@initLexical}} />
diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 42a03c70841..883c9d6f4bf 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -671,7 +671,12 @@ export default class KoenigLexicalEditor extends Component { const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { - this.args.initLexicalScratch(data); + try { + console.log('data-- ', data); + this.args.initLexical(data); + } catch (error) { + this.onError(error); // eslint-disable-line + } }; return (
diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 4b06c11a889..97f34d463de 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -43,7 +43,7 @@ const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ 'post.lexicalScratch', - 'post.initLexicalScratch', + // 'post.lexical', // the lexical object might be refreshed by the initial instance with new schema so it needs to be watched 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', @@ -406,8 +406,9 @@ export default class LexicalEditorController extends Controller { } @action - initLexicalScratch(data) { - this.set('post.initLexicalScratch', JSON.stringify(data)); + initLexical(data) { + this.post.set('lexical', JSON.stringify(data)); + this.post.set('hasDirtyAttributes', false); // we need to reset this so that the initilised state is not considered dirty } @action @@ -1032,7 +1033,6 @@ export default class LexicalEditorController extends Controller { // TODO: can these be `boundOneWay` on the model as per the other attrs? post.set('titleScratch', post.get('title')); post.set('lexicalScratch', post.get('lexical')); - // post.set('initLexicalScratch', post.get('lexical')); this._previousTagNames = this._tagNames; @@ -1259,7 +1259,7 @@ export default class LexicalEditorController extends Controller { } // scratch isn't an attr so needs a manual dirty check - let lexical = post.get('initLexicalScratch'); + let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); // additional guard in case we are trying to compare null with undefined diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index e7206b16e0f..1ffb06d8d0b 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -136,7 +136,6 @@ export default Model.extend(Comparable, ValidationEngine, { scratch: null, lexicalScratch: null, titleScratch: null, - initLexicalScratch: 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 723f83d6700..6a32fd2d932 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -79,7 +79,7 @@ @onEditorCreated={{this.setKoenigEditor}} @updateWordCount={{this.updateWordCount}} @updatePostTkCount={{this.updatePostTkCount}} - @initLexicalScratch={{this.initLexicalScratch}} + @initLexical={{this.initLexical}} @updateFeatureImageTkCount={{this.updateFeatureImageTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} From cece16da2e45cb0342f28d95b9ca540ebcd42fd4 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 1 Aug 2024 08:42:28 +0700 Subject: [PATCH 06/23] Fixed linting --- ghost/admin/app/components/koenig-lexical-editor.js | 4 ++-- ghost/admin/app/controllers/lexical-editor.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 883c9d6f4bf..e64f8d3a3dc 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -672,10 +672,10 @@ export default class KoenigLexicalEditor extends Component { const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { try { - console.log('data-- ', data); this.args.initLexical(data); } catch (error) { - this.onError(error); // eslint-disable-line + console.log('unable to initialise, skipping'); // eslint-disable-line + // This is a catch-all, however it's current implementation is to catch it when loading revisions } }; return ( diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 97f34d463de..031cc8e2d1c 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -1297,7 +1297,6 @@ export default class LexicalEditorController extends Controller { // 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()}; } From a3e13498625896edb389113513df6bbe10b35235 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 1 Aug 2024 10:01:32 +0700 Subject: [PATCH 07/23] Removed lexical watch prop --- ghost/admin/app/controllers/lexical-editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 031cc8e2d1c..a9b97e30e37 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -43,7 +43,6 @@ const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ 'post.lexicalScratch', - // 'post.lexical', // the lexical object might be refreshed by the initial instance with new schema so it needs to be watched 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', From 59e9ad4c3804edfee6958e51956d0d324f8bff0b Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 1 Aug 2024 13:24:52 +0700 Subject: [PATCH 08/23] Ensure restore revision doesnt toggle dirty state --- .../admin/app/components/koenig-lexical-editor.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index e64f8d3a3dc..69e1bcd9f21 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -669,17 +669,17 @@ export default class KoenigLexicalEditor extends Component { const multiplayerDocId = cardConfig.post.id; const multiplayerUsername = this.session.user.name; + const [hasInitialised, setHasInitialised] = React.useState(false); + const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { - try { + if (this.args.initLexical && isInitInstance && !hasInitialised) { this.args.initLexical(data); - } catch (error) { - console.log('unable to initialise, skipping'); // eslint-disable-line - // This is a catch-all, however it's current implementation is to catch it when loading revisions + setHasInitialised(true); } }; return ( -
+
Loading editor...

}> - + { + !hasInitialised && + }
From 4d39071cddc78eefd548a2d6dca3e697f837db77 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 1 Aug 2024 15:53:12 +0700 Subject: [PATCH 09/23] Removed interfering instances --- .../app/components/koenig-lexical-editor.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 69e1bcd9f21..7407d58a2fe 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -669,17 +669,14 @@ export default class KoenigLexicalEditor extends Component { const multiplayerDocId = cardConfig.post.id; const multiplayerUsername = this.session.user.name; - const [hasInitialised, setHasInitialised] = React.useState(false); - const KGEditorComponent = ({isInitInstance}) => { const handleInitInstance = (data) => { - if (this.args.initLexical && isInitInstance && !hasInitialised) { + if (this.args.initLexical && isInitInstance) { this.args.initLexical(data); - setHasInitialised(true); } }; return ( -
+
handleInitInstance(e) : this.args.onChange} - registerAPI={this.args.registerAPI} + registerAPI={isInitInstance ? null : this.args.registerAPI} /> - - + {} : this.args.updateWordCount} /> + {} : this.args.updatePostTkCount} />
); @@ -713,9 +710,7 @@ export default class KoenigLexicalEditor extends Component { Loading editor...

}> - { - !hasInitialised && - } +
From edd6ed5b697cd85bc06ddb5e9584064f65c623ca Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Fri, 2 Aug 2024 10:49:03 +0700 Subject: [PATCH 10/23] Added fix to avoid overwriting initial lexical prop --- .../components/gh-koenig-editor-lexical.hbs | 1 + .../app/components/koenig-lexical-editor.js | 2 +- .../app/components/modal-post-history.js | 1 + ghost/admin/app/controllers/lexical-editor.js | 53 ++++++++++++++----- ghost/admin/app/models/post.js | 3 ++ ghost/admin/app/templates/lexical-editor.hbs | 1 + 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 488335edf87..6665fd3fcb3 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -100,6 +100,7 @@ @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} @initLexical={{@initLexical}} + @initLexicalState={{@initLexicalState}} />
diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 7407d58a2fe..0d2a2901bef 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -682,7 +682,7 @@ export default class KoenigLexicalEditor extends Component { cardConfig={cardConfig} enableMultiplayer={enableMultiplayer} fileUploader={{useFileUpload, fileTypes}} - initialEditorState={this.args.lexical} + initialEditorState={isInitInstance ? this.args.initLexicalState || this.args.lexical : this.args.lexical} multiplayerUsername={multiplayerUsername} multiplayerDocId={multiplayerDocId} multiplayerEndpoint={multiplayerEndpoint} diff --git a/ghost/admin/app/components/modal-post-history.js b/ghost/admin/app/components/modal-post-history.js index 05aba6646e6..2f9618805cd 100644 --- a/ghost/admin/app/components/modal-post-history.js +++ b/ghost/admin/app/components/modal-post-history.js @@ -130,6 +130,7 @@ export default class ModalPostHistory extends Component { updateEditor: () => { const state = this.editorAPI.editorInstance.parseEditorState(revision.lexical); this.editorAPI.editorInstance.setEditorState(state); + set(this.post, 'initLexicalState', revision.lexical); }, closePostHistoryModal: () => { this.closeModal(); diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index a9b97e30e37..b085eaf03db 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -43,6 +43,7 @@ const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ 'post.lexicalScratch', + 'post.initLexicalState', 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', @@ -183,6 +184,8 @@ export default class LexicalEditorController extends Controller { _saveOnLeavePerformed = false; _previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes + _initLexical = null; + /* computed properties ---------------------------------------------------*/ @alias('model') @@ -287,6 +290,11 @@ export default class LexicalEditorController extends Controller { return titleTk + excerptTk + this.postTkCount + this.featureImageTkCount; } + @computed('post.initLexicalState') + get initLexicalState() { + return this.post.initLexicalState; + } + @action updateScratch(lexical) { this.set('post.lexicalScratch', JSON.stringify(lexical)); @@ -406,8 +414,8 @@ export default class LexicalEditorController extends Controller { @action initLexical(data) { - this.post.set('lexical', JSON.stringify(data)); - this.post.set('hasDirtyAttributes', false); // we need to reset this so that the initilised state is not considered dirty + this.post.set('initLexicalState', JSON.stringify(data)); + this._initLexical = JSON.stringify(data); } @action @@ -1261,24 +1269,45 @@ export default class LexicalEditorController extends Controller { let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); + // We have to get try get the init lexical from two area because, + // due to a race condition the initLexical in the post model might not be set yet + // and then in other cases such as restoring a past post, we can set the initLexical in the model + // which will then be used to compare with the scratch model + + let initLexical = post.get('initLexicalState') || this._initLexical; + // 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 + if (scratch || lexical || initLexical) { + // Initially compare initLexical with scratch + if (initLexical && scratch) { + const initLexicalChildNodes = initLexical ? JSON.parse(initLexical).root?.children : []; const scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; - const lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; - // // nullling is typically faster than delete + initLexicalChildNodes.forEach(child => child.direction = null); scratchChildNodes.forEach(child => child.direction = null); - lexicalChildNodes.forEach(child => child.direction = null); - if (JSON.stringify(scratchChildNodes) === JSON.stringify(lexicalChildNodes)) { + if (JSON.stringify(initLexicalChildNodes) !== JSON.stringify(scratchChildNodes)) { + this._leaveModalReason = {reason: 'initLexical is different from scratch', context: {initLexical, scratch}}; + return true; + } + + if (initLexical === scratch) { return false; } + } - this._leaveModalReason = {reason: 'lexical is different', context: {current: lexical, scratch}}; - return true; + // Future updates compare lexical with scratch + if (scratch && lexical) { + const scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; + const lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; + + scratchChildNodes.forEach(child => child.direction = null); + lexicalChildNodes.forEach(child => child.direction = null); + + if (JSON.stringify(scratchChildNodes) !== JSON.stringify(lexicalChildNodes)) { + this._leaveModalReason = {reason: 'lexical is different from scratch', context: {current: lexical, scratch}}; + return true; + } } } diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 1ffb06d8d0b..06dda4ee9c0 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -136,6 +136,9 @@ 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 + initLexicalState: 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 6a32fd2d932..ca0666c2aec 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -80,6 +80,7 @@ @updateWordCount={{this.updateWordCount}} @updatePostTkCount={{this.updatePostTkCount}} @initLexical={{this.initLexical}} + @initLexicalState={{this.initLexicalState}} @updateFeatureImageTkCount={{this.updateFeatureImageTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} From a1cb14c3d23794e0d163d0b0f27ea33effcd7f02 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Fri, 2 Aug 2024 13:14:53 +0700 Subject: [PATCH 11/23] Changed hasDirtyAttributes logic --- .../app/components/koenig-lexical-editor.js | 1 + ghost/admin/app/controllers/lexical-editor.js | 76 +++++++------------ 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 0d2a2901bef..158173fd2c3 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -675,6 +675,7 @@ export default class KoenigLexicalEditor extends Component { this.args.initLexical(data); } }; + return (
child.direction = null); - scratchChildNodes.forEach(child => child.direction = null); - - if (JSON.stringify(initLexicalChildNodes) !== JSON.stringify(scratchChildNodes)) { - this._leaveModalReason = {reason: 'initLexical is different from scratch', context: {initLexical, scratch}}; - return true; - } + let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; + let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; + let initLexicalChildNodes = initLexical ? JSON.parse(initLexical).root?.children : []; - if (initLexical === scratch) { - return false; - } - } + lexicalChildNodes.forEach(child => child.direction = null); + scratchChildNodes.forEach(child => child.direction = null); + initLexicalChildNodes.forEach(child => child.direction = null); - // Future updates compare lexical with scratch - if (scratch && lexical) { - const scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; - const lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; + // Compare initLexical with scratch + let isInitLexicalDirty = initLexical && scratch && JSON.stringify(initLexicalChildNodes) !== JSON.stringify(scratchChildNodes); - scratchChildNodes.forEach(child => child.direction = null); - lexicalChildNodes.forEach(child => child.direction = null); - - if (JSON.stringify(scratchChildNodes) !== JSON.stringify(lexicalChildNodes)) { - this._leaveModalReason = {reason: 'lexical is different from scratch', context: {current: lexical, scratch}}; - return true; - } - } - } + // Compare lexical with scratch + let isLexicalDirty = lexical && scratch && JSON.stringify(lexicalChildNodes) !== JSON.stringify(scratchChildNodes); - // 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()); @@ -1322,14 +1292,26 @@ 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 + if (!isInitLexicalDirty || !isLexicalDirty) { + return false; } - return hasDirtyAttributes; + // If both comparisons are dirty, consider the post dirty + if (isInitLexicalDirty && isLexicalDirty) { + this._leaveModalReason = {reason: 'initLexical and lexical are different from scratch', context: {initLexical, lexical, scratch}}; + return true; + } + + return false; } _showSaveNotification(prevStatus, status, delayed) { From abea2e24f10c48480c8a2e0038aee2f13922fa51 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Fri, 2 Aug 2024 13:24:24 +0700 Subject: [PATCH 12/23] Fixed test --- ghost/admin/tests/unit/controllers/editor-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 088f22391dd..658d6f3aef5 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -208,7 +208,8 @@ describe('Unit: Controller: lexical-editor', function () { titleScratch: 'this is a title', status: 'published', lexical: initialLexicalString, - lexicalScratch: initialLexicalString + lexicalScratch: initialLexicalString, + initLexicalState: initialLexicalString })); // synthetically update the lexicalScratch as if the editor itself made the modifications on loading the initial editorState From 8d7b346bfd7641e4bb3e6f1c0592a449e00fcba9 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Sat, 3 Aug 2024 12:05:50 +0700 Subject: [PATCH 13/23] Added second instance API to update revisions --- ghost/admin/app/components/gh-koenig-editor-lexical.hbs | 1 + ghost/admin/app/components/gh-koenig-editor-lexical.js | 7 +++++++ ghost/admin/app/components/gh-post-settings-menu.hbs | 1 + ghost/admin/app/components/koenig-lexical-editor.js | 4 ++-- ghost/admin/app/components/modal-post-history.hbs | 1 + ghost/admin/app/components/modal-post-history.js | 8 +++++++- ghost/admin/app/controllers/lexical-editor.js | 9 +++++++-- ghost/admin/app/templates/lexical-editor.hbs | 2 ++ 8 files changed, 28 insertions(+), 5 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 6665fd3fcb3..05fefde2c49 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -96,6 +96,7 @@ @cardConfig={{@cardOptions}} @onChange={{@onBodyChange}} @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 2319ae9c8a6..be28f73a5f7 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.js +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.js @@ -15,6 +15,7 @@ export default class GhKoenigEditorLexical extends Component { uploadUrl = `${ghostPaths().apiRoot}/images/upload/`; editorAPI = null; + secondaryEditorAPI = null; skipFocusEditor = false; @tracked titleIsHovered = false; @@ -232,6 +233,12 @@ 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 1d01dcc5bce..ee91b2af809 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.hbs +++ b/ghost/admin/app/components/gh-post-settings-menu.hbs @@ -853,6 +853,7 @@ 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 158173fd2c3..789116488fc 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -683,7 +683,7 @@ export default class KoenigLexicalEditor extends Component { cardConfig={cardConfig} enableMultiplayer={enableMultiplayer} fileUploader={{useFileUpload, fileTypes}} - initialEditorState={isInitInstance ? this.args.initLexicalState || this.args.lexical : this.args.lexical} + initialEditorState={this.args.lexical} multiplayerUsername={multiplayerUsername} multiplayerDocId={multiplayerDocId} multiplayerEndpoint={multiplayerEndpoint} @@ -697,7 +697,7 @@ export default class KoenigLexicalEditor extends Component { placeholderText={isInitInstance ? null : this.args.placeholderText} darkMode={isInitInstance ? null : this.feature.nightShift} onChange={isInitInstance ? e => handleInitInstance(e) : this.args.onChange} - registerAPI={isInitInstance ? null : this.args.registerAPI} + registerAPI={isInitInstance ? this.args.registerSecondaryAPI : this.args.registerAPI} /> {} : this.args.updateWordCount} /> {} : this.args.updatePostTkCount} /> diff --git a/ghost/admin/app/components/modal-post-history.hbs b/ghost/admin/app/components/modal-post-history.hbs index 3989c29db59..f7f8ab1261c 100644 --- a/ghost/admin/app/components/modal-post-history.hbs +++ b/ghost/admin/app/components/modal-post-history.hbs @@ -33,6 +33,7 @@ @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 2f9618805cd..4dfa987e2e7 100644 --- a/ghost/admin/app/components/modal-post-history.js +++ b/ghost/admin/app/components/modal-post-history.js @@ -31,6 +31,7 @@ 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; } @@ -101,6 +102,11 @@ export default class ModalPostHistory extends Component { this.selectedEditor = api; } + @action + registerSecondarySelectedEditorApi(api) { + this.secondarySelectedEditor = api; + } + @action registerComparisonEditorApi(api) { this.comparisonEditor = api; @@ -130,7 +136,7 @@ export default class ModalPostHistory extends Component { updateEditor: () => { const state = this.editorAPI.editorInstance.parseEditorState(revision.lexical); this.editorAPI.editorInstance.setEditorState(state); - set(this.post, 'initLexicalState', revision.lexical); + 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 1ee24c757e1..3b394dc7ce3 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -437,6 +437,11 @@ export default class LexicalEditorController extends Controller { this.editorAPI = API; } + @action + registerSecondaryEditorAPI(API) { + this.secondaryEditorAPI = API; + } + @action clearFeatureImage() { this.post.set('featureImage', null); @@ -1265,7 +1270,7 @@ export default class LexicalEditorController extends Controller { // Lexical and scratch comparison let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); - let initLexical = post.get('initLexicalState') || this._initLexical; + let initLexical = this.initLexicalState; let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; @@ -1300,7 +1305,7 @@ export default class LexicalEditorController extends Controller { return true; } - // If either comparison is not dirty, return false + // If either comparison is not dirty, return false, because we scratch is always up to date. if (!isInitLexicalDirty || !isLexicalDirty) { return false; } diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index ca0666c2aec..4ae962a785d 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -99,6 +99,7 @@ }} @postType={{this.post.displayName}} @registerAPI={{this.registerEditorAPI}} + @registerSecondaryAPI={{this.registerSecondaryEditorAPI}} @savePostTask={{this.savePostTask}} /> @@ -138,6 +139,7 @@ @updateSlugTask={{this.updateSlugTask}} @savePostTask={{this.savePostTask}} @editorAPI={{this.editorAPI}} + @secondaryEditorAPI={{this.secondaryEditorAPI}} @toggleSettingsMenu={{this.toggleSettingsMenu}} /> {{/if}} From b4db8b91005aad8677691b30a350da2ca4fc0618 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Sat, 3 Aug 2024 12:36:16 +0700 Subject: [PATCH 14/23] Cleanup --- ghost/admin/app/controllers/lexical-editor.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 3b394dc7ce3..1537a6cdcde 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -184,8 +184,6 @@ export default class LexicalEditorController extends Controller { _saveOnLeavePerformed = false; _previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes - _initLexical = null; - /* computed properties ---------------------------------------------------*/ @alias('model') @@ -414,8 +412,7 @@ export default class LexicalEditorController extends Controller { @action initLexical(data) { - this.post.set('initLexicalState', JSON.stringify(data)); - this._initLexical = JSON.stringify(data); + this.post.set('initLexicalState', JSON.stringify(data)); // sets the initial lexical state from the secondary editor } @action From 0fe1821aea7e4d9715ee5f1e368bf6c960a92250 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Sat, 3 Aug 2024 17:27:44 +0700 Subject: [PATCH 15/23] Cleanup --- ghost/admin/app/components/gh-koenig-editor-lexical.hbs | 1 - ghost/admin/app/templates/lexical-editor.hbs | 1 - 2 files changed, 2 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index 05fefde2c49..cb4eb65d3ac 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -101,7 +101,6 @@ @updateWordCount={{@updateWordCount}} @updatePostTkCount={{@updatePostTkCount}} @initLexical={{@initLexical}} - @initLexicalState={{@initLexicalState}} /> diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index 4ae962a785d..8e0d537ec4a 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -80,7 +80,6 @@ @updateWordCount={{this.updateWordCount}} @updatePostTkCount={{this.updatePostTkCount}} @initLexical={{this.initLexical}} - @initLexicalState={{this.initLexicalState}} @updateFeatureImageTkCount={{this.updateFeatureImageTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} From e86df3037715e1fc61dc6bcc8ef21fbaa5ae3c15 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Sat, 3 Aug 2024 17:36:47 +0700 Subject: [PATCH 16/23] Added logic test --- .../tests/unit/controllers/editor-test.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 658d6f3aef5..b7d88830bd1 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -226,5 +226,25 @@ describe('Unit: Controller: lexical-editor', function () { isDirty = controller.get('hasDirtyAttributes'); expect(isDirty).to.be.true; }); + + it.only('dirty is false if initlexical 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 lexicalStringNoNullDirection = `{"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 lexicalStringUpdatedContent = `{"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: lexicalStringNoNullDirection, + initLexicalState: lexicalStringUpdatedContent + })); + + let isDirty = controller.get('hasDirtyAttributes'); + + expect(isDirty).to.be.false; + }); }); }); From c748f084fd7dbac9cbb5d89dc94514e548a4fe46 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 08:56:29 +0700 Subject: [PATCH 17/23] Removed only --- ghost/admin/tests/unit/controllers/editor-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index b7d88830bd1..614b8afcbd1 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -227,7 +227,7 @@ describe('Unit: Controller: lexical-editor', function () { expect(isDirty).to.be.true; }); - it.only('dirty is false if initlexical and scratch matches, but lexical is outdated', async function () { + it('dirty is false if initlexical 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 lexicalStringNoNullDirection = `{"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 lexicalStringUpdatedContent = `{"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}}`; From 4c2491f3f5fcb79dc6d8c0986f14b6543b76b034 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 09:07:25 +0700 Subject: [PATCH 18/23] Added test --- .../tests/unit/controllers/editor-test.js | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 614b8afcbd1..d4532878dd6 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -229,8 +229,8 @@ describe('Unit: Controller: lexical-editor', function () { it('dirty is false if initlexical 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 lexicalStringNoNullDirection = `{"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 lexicalStringUpdatedContent = `{"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}}`; + 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({ @@ -238,13 +238,35 @@ describe('Unit: Controller: lexical-editor', function () { titleScratch: 'this is a title', status: 'published', lexical: initialLexicalString, - lexicalScratch: lexicalStringNoNullDirection, - initLexicalState: lexicalStringUpdatedContent + lexicalScratch: lexicalScratch, + initLexicalState: secondLexicalInstance })); let isDirty = controller.get('hasDirtyAttributes'); expect(isDirty).to.be.false; }); + + it('dirty is true if initLexical 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, + initLexicalState: secondLexicalInstance + })); + + controller.send('updateScratch',JSON.parse(lexicalScratch)); + + let isDirty = controller.get('hasDirtyAttributes'); + + expect(isDirty).to.be.true; + }); }); }); From 662e9b2ddd8b7f9675d37f11a1dd5f9b6602f4e2 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 09:20:48 +0700 Subject: [PATCH 19/23] Pass editor instance for comparison --- ghost/admin/app/controllers/lexical-editor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 1537a6cdcde..0f106451193 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -1267,7 +1267,10 @@ export default class LexicalEditorController extends Controller { // Lexical and scratch comparison let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); - let initLexical = this.initLexicalState; + // let initLexical = this.initLexicalState; + let initLexical = this.secondaryEditorAPI?.editorInstance?.getEditorState().toJSON(); + // convert initLexical to string + initLexical = JSON.stringify(initLexical); let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; From 5f9207523acc6a5e93f512334cda246c21d2df33 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 10:12:55 +0700 Subject: [PATCH 20/23] Cleaned up --- .../components/gh-koenig-editor-lexical.hbs | 2 +- .../app/components/koenig-lexical-editor.js | 8 +---- ghost/admin/app/controllers/lexical-editor.js | 35 ++++++++----------- ghost/admin/app/models/post.js | 2 +- ghost/admin/app/templates/lexical-editor.hbs | 2 +- .../tests/unit/controllers/editor-test.js | 6 ++-- 6 files changed, 21 insertions(+), 34 deletions(-) diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index cb4eb65d3ac..3ad4aae7a00 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -95,12 +95,12 @@ @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}} - @initLexical={{@initLexical}} /> diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 789116488fc..8f91477f74c 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -670,12 +670,6 @@ export default class KoenigLexicalEditor extends Component { const multiplayerUsername = this.session.user.name; const KGEditorComponent = ({isInitInstance}) => { - const handleInitInstance = (data) => { - if (this.args.initLexical && isInitInstance) { - this.args.initLexical(data); - } - }; - return (
handleInitInstance(e) : this.args.onChange} + onChange={isInitInstance ? this.args.updateSecondaryInstanceModel : this.args.onChange} registerAPI={isInitInstance ? this.args.registerSecondaryAPI : this.args.registerAPI} /> {} : this.args.updateWordCount} /> diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 0f106451193..e5e8f58f24b 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -43,7 +43,6 @@ const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ 'post.lexicalScratch', - 'post.initLexicalState', 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', @@ -288,11 +287,6 @@ export default class LexicalEditorController extends Controller { return titleTk + excerptTk + this.postTkCount + this.featureImageTkCount; } - @computed('post.initLexicalState') - get initLexicalState() { - return this.post.initLexicalState; - } - @action updateScratch(lexical) { this.set('post.lexicalScratch', JSON.stringify(lexical)); @@ -303,6 +297,11 @@ 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); @@ -410,11 +409,6 @@ export default class LexicalEditorController extends Controller { this.set('postTkCount', count); } - @action - initLexical(data) { - this.post.set('initLexicalState', JSON.stringify(data)); // sets the initial lexical state from the secondary editor - } - @action updateFeatureImageTkCount(count) { this.set('featureImageTkCount', count); @@ -1267,21 +1261,20 @@ export default class LexicalEditorController extends Controller { // Lexical and scratch comparison let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); - // let initLexical = this.initLexicalState; - let initLexical = this.secondaryEditorAPI?.editorInstance?.getEditorState().toJSON(); - // convert initLexical to string - initLexical = JSON.stringify(initLexical); + let secondaryLexical = post.get('secondaryLexicalState'); + // let initLexical = this.secondaryEditorAPI?.editorInstance?.getEditorState().toJSON(); + // initLexical = JSON.stringify(initLexical); let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; - let initLexicalChildNodes = initLexical ? JSON.parse(initLexical).root?.children : []; + let secondaryLexicalChildNodes = secondaryLexical ? JSON.parse(secondaryLexical).root?.children : []; lexicalChildNodes.forEach(child => child.direction = null); scratchChildNodes.forEach(child => child.direction = null); - initLexicalChildNodes.forEach(child => child.direction = null); + secondaryLexicalChildNodes.forEach(child => child.direction = null); // Compare initLexical with scratch - let isInitLexicalDirty = initLexical && scratch && JSON.stringify(initLexicalChildNodes) !== JSON.stringify(scratchChildNodes); + let isSecondaryDirty = secondaryLexical && scratch && JSON.stringify(secondaryLexicalChildNodes) !== JSON.stringify(scratchChildNodes); // Compare lexical with scratch let isLexicalDirty = lexical && scratch && JSON.stringify(lexicalChildNodes) !== JSON.stringify(scratchChildNodes); @@ -1306,13 +1299,13 @@ export default class LexicalEditorController extends Controller { } // If either comparison is not dirty, return false, because we scratch is always up to date. - if (!isInitLexicalDirty || !isLexicalDirty) { + if (!isSecondaryDirty || !isLexicalDirty) { return false; } // If both comparisons are dirty, consider the post dirty - if (isInitLexicalDirty && isLexicalDirty) { - this._leaveModalReason = {reason: 'initLexical and lexical are different from scratch', context: {initLexical, lexical, scratch}}; + if (isSecondaryDirty && isLexicalDirty) { + this._leaveModalReason = {reason: 'initLexical and lexical are different from scratch', context: {secondaryLexical, lexical, scratch}}; return true; } diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 06dda4ee9c0..835d24d0a2f 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -138,7 +138,7 @@ export default Model.extend(Comparable, ValidationEngine, { 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 - initLexicalState: null, + 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 8e0d537ec4a..b7ee9a4a747 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -73,13 +73,13 @@ @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" @onEditorCreated={{this.setKoenigEditor}} @updateWordCount={{this.updateWordCount}} @updatePostTkCount={{this.updatePostTkCount}} - @initLexical={{this.initLexical}} @updateFeatureImageTkCount={{this.updateFeatureImageTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index d4532878dd6..3ff6d45f0ef 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -209,7 +209,7 @@ describe('Unit: Controller: lexical-editor', function () { status: 'published', lexical: initialLexicalString, lexicalScratch: initialLexicalString, - initLexicalState: initialLexicalString + secondaryLexicalState: initialLexicalString })); // synthetically update the lexicalScratch as if the editor itself made the modifications on loading the initial editorState @@ -239,7 +239,7 @@ describe('Unit: Controller: lexical-editor', function () { status: 'published', lexical: initialLexicalString, lexicalScratch: lexicalScratch, - initLexicalState: secondLexicalInstance + secondaryLexicalState: secondLexicalInstance })); let isDirty = controller.get('hasDirtyAttributes'); @@ -259,7 +259,7 @@ describe('Unit: Controller: lexical-editor', function () { status: 'published', lexical: initialLexicalString, lexicalScratch: lexicalScratch, - initLexicalState: secondLexicalInstance + secondaryLexicalState: secondLexicalInstance })); controller.send('updateScratch',JSON.parse(lexicalScratch)); From f45932f3c9d87d47909208a10f35846cdb7aafa1 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 10:18:10 +0700 Subject: [PATCH 21/23] Cleanup --- ghost/admin/app/controllers/lexical-editor.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index e5e8f58f24b..8dd885a0e0a 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -1262,8 +1262,6 @@ export default class LexicalEditorController extends Controller { let lexical = post.get('lexical'); let scratch = post.get('lexicalScratch'); let secondaryLexical = post.get('secondaryLexicalState'); - // let initLexical = this.secondaryEditorAPI?.editorInstance?.getEditorState().toJSON(); - // initLexical = JSON.stringify(initLexical); let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : []; let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : []; @@ -1298,7 +1296,7 @@ export default class LexicalEditorController extends Controller { return true; } - // If either comparison is not dirty, return false, because we scratch is always up to date. + // If either comparison is not dirty, return false, because scratch is always up to date. if (!isSecondaryDirty || !isLexicalDirty) { return false; } From ce716243121a3b08bd735d0614713cbaafe100b0 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 11:20:05 +0700 Subject: [PATCH 22/23] Fixed test description --- ghost/admin/tests/unit/controllers/editor-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/tests/unit/controllers/editor-test.js b/ghost/admin/tests/unit/controllers/editor-test.js index 3ff6d45f0ef..acf7a5e95b0 100644 --- a/ghost/admin/tests/unit/controllers/editor-test.js +++ b/ghost/admin/tests/unit/controllers/editor-test.js @@ -227,7 +227,7 @@ describe('Unit: Controller: lexical-editor', function () { expect(isDirty).to.be.true; }); - it('dirty is false if initlexical and scratch matches, but lexical is outdated', async function () { + 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}}`; @@ -247,7 +247,7 @@ describe('Unit: Controller: lexical-editor', function () { expect(isDirty).to.be.false; }); - it('dirty is true if initLexical and lexical does not match scratch', async function () { + 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}}`; From 3b6deedd06374d4e25d8d2414bb154bda70e141f Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Mon, 5 Aug 2024 19:52:02 +0700 Subject: [PATCH 23/23] Fixed comment --- ghost/admin/app/controllers/lexical-editor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 8dd885a0e0a..2b8342afd9e 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -1244,7 +1244,8 @@ export default class LexicalEditorController extends Controller { return true; } - // Post tags comparison + // post.tags is an array so hasDirtyAttributes doesn't pick up + // changes unless the array ref is changed let currentTags = (this._tagNames || []).join(', '); let previousTags = (this._previousTagNames || []).join(', '); if (currentTags !== previousTags) {