diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index bed3c50e2d..a8efb4d02e 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -28,6 +28,15 @@ {{/if}} + {{#if (and (feature "tkReminders") @titleHasTk)}} +
+ TK +
+ {{/if}} + diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 5f6238ef6a..3935a8d99b 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -536,7 +536,7 @@ export default class KoenigLexicalEditor extends Component { registerAPI={this.args.registerAPI} /> - {this.feature.tkReminders && } + {this.feature.tkReminders && } diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index a85e31b9c6..ce21935936 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -35,6 +35,9 @@ const AUTOSAVE_TIMEOUT = 3000; // time in ms to force a save if the user is continuously typing const TIMEDSAVE_TIMEOUT = 60000; +const TK_REGEX = new RegExp(/(^|.)([^\p{L}\p{N}\s]*(TK)+[^\p{L}\p{N}\s]*)(.)?/u); +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', @@ -127,7 +130,7 @@ export default class LexicalEditorController extends Controller { // koenig related properties wordCount = 0; - tkCount = 0; + postTkCount = 0; /* private properties ----------------------------------------------------*/ @@ -215,6 +218,55 @@ export default class LexicalEditorController extends Controller { return config.environment !== 'test' && this.get('post.isDraft'); } + TK_REGEX = new RegExp(/(^|.)([^\p{L}\p{N}\s]*(TK)+[^\p{L}\p{N}\s]*)(.)?/u); + WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u); + + @computed('post.titleScratch') + get titleHasTk() { + let text = this.post.titleScratch; + let matchArr = TK_REGEX.exec(text); + + if (matchArr === null) { + return false; + } + + function isValidMatch(match) { + // negative lookbehind isn't supported before Safari 16.4 + // so we capture the preceding char and test it here + if (match[1] && match[1].trim() && WORD_CHAR_REGEX.test(match[1])) { + return false; + } + + // we also check any following char in code to avoid an overly + // complex regex when looking for word-chars following the optional + // trailing symbol char + if (match[4] && match[4].trim() && WORD_CHAR_REGEX.test(match[4])) { + return false; + } + + return true; + } + + // our regex will match invalid TKs because we can't use negative lookbehind + // so we need to loop through the matches discarding any that are invalid + // and moving on to any subsequent matches + while (matchArr !== null && !isValidMatch(matchArr)) { + text = text.slice(matchArr.index + matchArr[0].length - 1); + matchArr = TK_REGEX.exec(text); + } + + if (matchArr === null) { + return false; + } + + return true; + } + + @computed('titleHasTk', 'postTkCount') + get tkCount() { + return (this.titleHasTk ? 1 : 0) + this.postTkCount; + } + @action updateScratch(lexical) { this.set('post.lexicalScratch', JSON.stringify(lexical)); @@ -312,8 +364,8 @@ export default class LexicalEditorController extends Controller { } @action - updateTkCount(count) { - this.set('tkCount', count); + updatePostTkCount(count) { + this.set('postTkCount', count); } @action @@ -1082,7 +1134,7 @@ export default class LexicalEditorController extends Controller { this.set('shouldFocusTitle', false); this.set('showSettingsMenu', false); this.set('wordCount', 0); - this.set('tkCount', 0); + this.set('postTkCount', 0); // remove the onbeforeunload handler as it's only relevant whilst on // the editor route diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index 6c944f07ee..2af885244b 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -683,6 +683,17 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone { opacity: .5; } +.gh-editor-title-container .tk-indicator { + position: absolute; + top: 15px; + right: -5.6rem; + padding: .4rem; + color: #95A1AD; + font-size: 1.2rem; + font-weight: 500; + cursor: default; +} + .gh-editor-back-button { height: 34px; margin-right: 8px; diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index 3639085ef5..d0cb2d14b2 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -67,6 +67,7 @@ @title={{readonly this.post.titleScratch}} @titleAutofocus={{this.shouldFocusTitle}} @titlePlaceholder={{concat (capitalize this.post.displayName) " title"}} + @titleHasTk={{this.titleHasTk}} @onTitleChange={{this.updateTitleScratch}} @onTitleBlur={{perform this.saveTitleTask}} @body={{readonly this.post.lexicalScratch}} @@ -77,7 +78,7 @@ @scrollOffsetBottomSelector=".gh-mobile-nav-bar" @onEditorCreated={{this.setKoenigEditor}} @updateWordCount={{this.updateWordCount}} - @updateTkCount={{this.updateTkCount}} + @updatePostTkCount={{this.updatePostTkCount}} @featureImage={{this.post.featureImage}} @featureImageAlt={{this.post.featureImageAlt}} @featureImageCaption={{this.post.featureImageCaption}}