From f68347f92c9ea10bd9e26f50903ac40785c78ba8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 17 Mar 2025 22:46:00 +0200 Subject: [PATCH] client: port ts --- src/public/app/types.d.ts | 26 +++++- .../{find_in_code.js => find_in_code.ts} | 82 +++++++++++++------ .../{find_in_html.js => find_in_html.ts} | 38 +++++---- .../{find_in_text.js => find_in_text.ts} | 46 +++++++---- .../app/widgets/note_context_aware_widget.ts | 3 +- 5 files changed, 138 insertions(+), 57 deletions(-) rename src/public/app/widgets/{find_in_code.js => find_in_code.ts} (80%) rename src/public/app/widgets/{find_in_html.js => find_in_html.ts} (69%) rename src/public/app/widgets/{find_in_text.js => find_in_text.ts} (78%) diff --git a/src/public/app/types.d.ts b/src/public/app/types.d.ts index 0252c9d37..63c9acdde 100644 --- a/src/public/app/types.d.ts +++ b/src/public/app/types.d.ts @@ -95,7 +95,11 @@ declare global { className: string; separateWordSearch: boolean; caseSensitive: boolean; - }) + done: () => void; + }); + unmark(opts?: { + done: () => void; + }); } interface JQueryStatic { @@ -221,9 +225,23 @@ declare global { setOption(name: string, value: string); refresh(); focus(); + getCursor(): { line: number, col: number, ch: number }; setCursor(line: number, col: number); lineCount(): number; on(event: string, callback: () => void); + operation(callback: () => void); + scrollIntoView(pos: number); + doc: { + getValue(): string; + markText( + from: { line: number, ch: number } | number, + to: { line: number, ch: number } | number, + opts: { + className: string + }); + setSelection(from: number, to: number); + replaceRange(text: string, from: number, to: number); + } } var katex: { @@ -260,6 +278,7 @@ declare global { getRoot(): TextEditorElement; selection: { getFirstPosition(): undefined | TextPosition; + getLastPosition(): undefined | TextPosition; } }, change(cb: (writer: Writer) => void) @@ -283,13 +302,16 @@ declare global { } }; } - change(cb: (writer: Writer) => void) + change(cb: (writer: Writer) => void); + scrollToTheSelection(): void; } }, getData(): string; setData(data: string): void; getSelectedHtml(): string; removeSelection(): void; + execute(action: string, ...args: unknown[]): void; + focus(): void; sourceElement: HTMLElement; } diff --git a/src/public/app/widgets/find_in_code.js b/src/public/app/widgets/find_in_code.ts similarity index 80% rename from src/public/app/widgets/find_in_code.js rename to src/public/app/widgets/find_in_code.ts index da3476722..d29cd67ce 100644 --- a/src/public/app/widgets/find_in_code.js +++ b/src/public/app/widgets/find_in_code.ts @@ -2,35 +2,54 @@ // uses for highlighting matches, use the same one on CodeMirror // for consistency import utils from "../services/utils.js"; +import type FindWidget from "./find.js"; const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; +// TODO: Deduplicate. +interface Match { + className: string; + clear(): void; + find(): { + from: number; + to: number; + }; +} + export default class FindInCode { - constructor(parent) { - /** @property {FindWidget} */ + + private parent: FindWidget; + private findResult?: Match[] | null; + + constructor(parent: FindWidget) { this.parent = parent; } async getCodeEditor() { - return this.parent.noteContext.getCodeEditor(); + return this.parent.noteContext?.getCodeEditor(); } - async performFind(searchTerm, matchCase, wholeWord) { - let findResult = null; + async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { + let findResult: Match[] | null = null; let totalFound = 0; let currentFound = -1; // See https://codemirror.net/addon/search/searchcursor.js for tips const codeEditor = await this.getCodeEditor(); + if (!codeEditor) { + return; + } + const doc = codeEditor.doc; const text = doc.getValue(); // Clear all markers - if (this.findResult != null) { + if (this.findResult) { codeEditor.operation(() => { - for (let i = 0; i < this.findResult.length; ++i) { - const marker = this.findResult[i]; + const findResult = this.findResult as Match[]; + for (let i = 0; i < findResult.length; ++i) { + const marker = findResult[i]; marker.clear(); } }); @@ -49,7 +68,7 @@ export default class FindInCode { const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i")); let curLine = 0; let curChar = 0; - let curMatch = null; + let curMatch: RegExpExecArray | null = null; findResult = []; // All those markText take several seconds on e.g., this ~500-line // script, batch them inside an operation, so they become @@ -73,7 +92,7 @@ export default class FindInCode { let toPos = { line: curLine, ch: curChar + curMatch[0].length }; // or css = "color: #f3" let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME }); - findResult.push(marker); + findResult?.push(marker); // Set the first match beyond the cursor as the current match if (currentFound === -1) { @@ -99,7 +118,7 @@ export default class FindInCode { this.findResult = findResult; // Calculate curfound if not already, highlight it as selected - if (totalFound > 0) { + if (findResult && totalFound > 0) { currentFound = Math.max(0, currentFound); let marker = findResult[currentFound]; let pos = marker.find(); @@ -114,8 +133,12 @@ export default class FindInCode { }; } - async findNext(direction, currentFound, nextFound) { + async findNext(direction: number, currentFound: number, nextFound: number) { const codeEditor = await this.getCodeEditor(); + if (!codeEditor || !this.findResult) { + return; + } + const doc = codeEditor.doc; // @@ -137,18 +160,23 @@ export default class FindInCode { codeEditor.scrollIntoView(pos.from); } - async findBoxClosed(totalFound, currentFound) { + async findBoxClosed(totalFound: number, currentFound: number) { const codeEditor = await this.getCodeEditor(); - if (totalFound > 0) { + if (codeEditor && totalFound > 0) { const doc = codeEditor.doc; - const pos = this.findResult[currentFound].find(); + const pos = this.findResult?.[currentFound].find(); // Note setting the selection sets the cursor to // the end of the selection and scrolls it into // view - doc.setSelection(pos.from, pos.to); + if (pos) { + doc.setSelection(pos.from, pos.to); + } // Clear all markers codeEditor.operation(() => { + if (!this.findResult) { + return; + } for (let i = 0; i < this.findResult.length; ++i) { let marker = this.findResult[i]; marker.clear(); @@ -157,9 +185,9 @@ export default class FindInCode { } this.findResult = null; - codeEditor.focus(); + codeEditor?.focus(); } - async replace(replaceText) { + async replace(replaceText: string) { // this.findResult may be undefined and null if (!this.findResult || this.findResult.length === 0) { return; @@ -178,8 +206,10 @@ export default class FindInCode { let marker = this.findResult[currentFound]; let pos = marker.find(); const codeEditor = await this.getCodeEditor(); - const doc = codeEditor.doc; - doc.replaceRange(replaceText, pos.from, pos.to); + const doc = codeEditor?.doc; + if (doc) { + doc.replaceRange(replaceText, pos.from, pos.to); + } marker.clear(); let nextFound; @@ -194,17 +224,21 @@ export default class FindInCode { } } } - async replaceAll(replaceText) { + async replaceAll(replaceText: string) { if (!this.findResult || this.findResult.length === 0) { return; } const codeEditor = await this.getCodeEditor(); - const doc = codeEditor.doc; - codeEditor.operation(() => { + const doc = codeEditor?.doc; + codeEditor?.operation(() => { + if (!this.findResult) { + return; + } + for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) { let marker = this.findResult[currentFound]; let pos = marker.find(); - doc.replaceRange(replaceText, pos.from, pos.to); + doc?.replaceRange(replaceText, pos.from, pos.to); marker.clear(); } }); diff --git a/src/public/app/widgets/find_in_html.js b/src/public/app/widgets/find_in_html.ts similarity index 69% rename from src/public/app/widgets/find_in_html.js rename to src/public/app/widgets/find_in_html.ts index 1c4b80971..4940d80eb 100644 --- a/src/public/app/widgets/find_in_html.js +++ b/src/public/app/widgets/find_in_html.ts @@ -4,28 +4,33 @@ import libraryLoader from "../services/library_loader.js"; import utils from "../services/utils.js"; import appContext from "../components/app_context.js"; +import type FindWidget from "./find.js"; const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; export default class FindInHtml { - constructor(parent) { - /** @property {FindWidget} */ + + private parent: FindWidget; + private currentIndex: number; + private $results: JQuery | null; + + constructor(parent: FindWidget) { this.parent = parent; this.currentIndex = 0; this.$results = null; } - async performFind(searchTerm, matchCase, wholeWord) { + async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { await libraryLoader.requireLibrary(libraryLoader.MARKJS); - const $content = await this.parent.noteContext.getContentElement(); + const $content = await this.parent?.noteContext?.getContentElement(); const wholeWordChar = wholeWord ? "\\b" : ""; const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); return new Promise((res) => { - $content.unmark({ + $content?.unmark({ done: () => { $content.markRegExp(regExp, { element: "span", @@ -48,8 +53,8 @@ export default class FindInHtml { }); } - async findNext(direction, currentFound, nextFound) { - if (this.$results.length) { + async findNext(direction: -1 | 1, currentFound: number, nextFound: number) { + if (this.$results?.length) { this.currentIndex += direction; if (this.currentIndex < 0) { @@ -64,13 +69,15 @@ export default class FindInHtml { } } - async findBoxClosed(totalFound, currentFound) { - const $content = await this.parent.noteContext.getContentElement(); - $content.unmark(); + async findBoxClosed(totalFound: number, currentFound: number) { + const $content = await this.parent?.noteContext?.getContentElement(); + if ($content) { + $content.unmark(); + } } async jumpTo() { - if (this.$results.length) { + if (this.$results?.length) { const offsetTop = 100; const $current = this.$results.eq(this.currentIndex); this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); @@ -79,10 +86,11 @@ export default class FindInHtml { $current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); const position = $current.position().top - offsetTop; - const $content = await this.parent.noteContext.getContentElement(); - const $contentWiget = appContext.getComponentByEl($content); - - $contentWiget.triggerCommand("scrollContainerTo", { position }); + const $content = await this.parent.noteContext?.getContentElement(); + if ($content) { + const $contentWidget = appContext.getComponentByEl($content[0]); + $contentWidget.triggerCommand("scrollContainerTo", { position }); + } } } } diff --git a/src/public/app/widgets/find_in_text.js b/src/public/app/widgets/find_in_text.ts similarity index 78% rename from src/public/app/widgets/find_in_text.js rename to src/public/app/widgets/find_in_text.ts index 9ce9c7c40..003f11be8 100644 --- a/src/public/app/widgets/find_in_text.js +++ b/src/public/app/widgets/find_in_text.ts @@ -1,14 +1,29 @@ +import type FindWidget from "./find.js"; + +// TODO: Deduplicate. +interface Match { + className: string; + clear(): void; + find(): { + from: number; + to: number; + }; +} + export default class FindInText { - constructor(parent) { - /** @property {FindWidget} */ + + private parent: FindWidget; + private findResult?: Match[] | null; + + constructor(parent: FindWidget) { this.parent = parent; } async getTextEditor() { - return this.parent.noteContext.getTextEditor(); + return this.parent?.noteContext?.getTextEditor(); } - async performFind(searchTerm, matchCase, wholeWord) { + async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { // Do this even if the searchTerm is empty so the markers are cleared and // the counters updated const textEditor = await this.getTextEditor(); @@ -54,7 +69,7 @@ export default class FindInText { // XXX Do this accessing the private data? // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js for (let i = 0; i < currentFound; ++i) { - textEditor.execute("findNext", searchTerm); + textEditor?.execute("findNext", searchTerm); } } @@ -64,7 +79,7 @@ export default class FindInText { }; } - async findNext(direction, currentFound, nextFound) { + async findNext(direction: number, currentFound: number, nextFound: number) { const textEditor = await this.getTextEditor(); // There are no parameters for findNext/findPrev @@ -72,20 +87,23 @@ export default class FindInText { // curFound wrap around above assumes findNext and // findPrevious wraparound, which is what they do if (direction > 0) { - textEditor.execute("findNext"); + textEditor?.execute("findNext"); } else { - textEditor.execute("findPrevious"); + textEditor?.execute("findPrevious"); } } - async findBoxClosed(totalFound, currentFound) { + async findBoxClosed(totalFound: number, currentFound: number) { const textEditor = await this.getTextEditor(); + if (!textEditor) { + return; + } if (totalFound > 0) { // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - const range = this.findResult.results.get(currentFound).marker.getRange(); + const range = this.findResult?.results?.get(currentFound).marker.getRange(); // From // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // XXX Roll our own since already done for codeEditor and @@ -104,17 +122,17 @@ export default class FindInText { textEditor.focus(); } - async replace(replaceText) { + async replace(replaceText: string) { if (this.editingState !== undefined && this.editingState.highlightedResult !== null) { const textEditor = await this.getTextEditor(); - textEditor.execute("replace", replaceText, this.editingState.highlightedResult); + textEditor?.execute("replace", replaceText, this.editingState.highlightedResult); } } - async replaceAll(replaceText) { + async replaceAll(replaceText: string) { if (this.editingState !== undefined && this.editingState.results.length > 0) { const textEditor = await this.getTextEditor(); - textEditor.execute("replaceAll", replaceText, this.editingState.results); + textEditor?.execute("replaceAll", replaceText, this.editingState.results); } } } diff --git a/src/public/app/widgets/note_context_aware_widget.ts b/src/public/app/widgets/note_context_aware_widget.ts index 3c1153c2f..620600f9a 100644 --- a/src/public/app/widgets/note_context_aware_widget.ts +++ b/src/public/app/widgets/note_context_aware_widget.ts @@ -5,10 +5,9 @@ import type NoteContext from "../components/note_context.js"; /** * This widget allows for changing and updating depending on the active note. - * @extends {BasicWidget} */ class NoteContextAwareWidget extends BasicWidget { - protected noteContext?: NoteContext; + noteContext?: NoteContext; isNoteContext(ntxId: string | string[] | null | undefined) { if (Array.isArray(ntxId)) {