client: port ts

This commit is contained in:
Elian Doran 2025-03-17 22:46:00 +02:00
parent 4f7f7c460a
commit f68347f92c
No known key found for this signature in database
5 changed files with 138 additions and 57 deletions

View File

@ -95,7 +95,11 @@ declare global {
className: string; className: string;
separateWordSearch: boolean; separateWordSearch: boolean;
caseSensitive: boolean; caseSensitive: boolean;
}) done: () => void;
});
unmark(opts?: {
done: () => void;
});
} }
interface JQueryStatic { interface JQueryStatic {
@ -221,9 +225,23 @@ declare global {
setOption(name: string, value: string); setOption(name: string, value: string);
refresh(); refresh();
focus(); focus();
getCursor(): { line: number, col: number, ch: number };
setCursor(line: number, col: number); setCursor(line: number, col: number);
lineCount(): number; lineCount(): number;
on(event: string, callback: () => void); 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: { var katex: {
@ -260,6 +278,7 @@ declare global {
getRoot(): TextEditorElement; getRoot(): TextEditorElement;
selection: { selection: {
getFirstPosition(): undefined | TextPosition; getFirstPosition(): undefined | TextPosition;
getLastPosition(): undefined | TextPosition;
} }
}, },
change(cb: (writer: Writer) => void) 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; getData(): string;
setData(data: string): void; setData(data: string): void;
getSelectedHtml(): string; getSelectedHtml(): string;
removeSelection(): void; removeSelection(): void;
execute(action: string, ...args: unknown[]): void;
focus(): void;
sourceElement: HTMLElement; sourceElement: HTMLElement;
} }

View File

@ -2,35 +2,54 @@
// uses for highlighting matches, use the same one on CodeMirror // uses for highlighting matches, use the same one on CodeMirror
// for consistency // for consistency
import utils from "../services/utils.js"; 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_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; 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 { export default class FindInCode {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private findResult?: Match[] | null;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
} }
async getCodeEditor() { async getCodeEditor() {
return this.parent.noteContext.getCodeEditor(); return this.parent.noteContext?.getCodeEditor();
} }
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
let findResult = null; let findResult: Match[] | null = null;
let totalFound = 0; let totalFound = 0;
let currentFound = -1; let currentFound = -1;
// See https://codemirror.net/addon/search/searchcursor.js for tips // See https://codemirror.net/addon/search/searchcursor.js for tips
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (!codeEditor) {
return;
}
const doc = codeEditor.doc; const doc = codeEditor.doc;
const text = doc.getValue(); const text = doc.getValue();
// Clear all markers // Clear all markers
if (this.findResult != null) { if (this.findResult) {
codeEditor.operation(() => { codeEditor.operation(() => {
for (let i = 0; i < this.findResult.length; ++i) { const findResult = this.findResult as Match[];
const marker = this.findResult[i]; for (let i = 0; i < findResult.length; ++i) {
const marker = findResult[i];
marker.clear(); marker.clear();
} }
}); });
@ -49,7 +68,7 @@ export default class FindInCode {
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i")); const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
let curLine = 0; let curLine = 0;
let curChar = 0; let curChar = 0;
let curMatch = null; let curMatch: RegExpExecArray | null = null;
findResult = []; findResult = [];
// All those markText take several seconds on e.g., this ~500-line // All those markText take several seconds on e.g., this ~500-line
// script, batch them inside an operation, so they become // 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 }; let toPos = { line: curLine, ch: curChar + curMatch[0].length };
// or css = "color: #f3" // or css = "color: #f3"
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME }); 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 // Set the first match beyond the cursor as the current match
if (currentFound === -1) { if (currentFound === -1) {
@ -99,7 +118,7 @@ export default class FindInCode {
this.findResult = findResult; this.findResult = findResult;
// Calculate curfound if not already, highlight it as selected // Calculate curfound if not already, highlight it as selected
if (totalFound > 0) { if (findResult && totalFound > 0) {
currentFound = Math.max(0, currentFound); currentFound = Math.max(0, currentFound);
let marker = findResult[currentFound]; let marker = findResult[currentFound];
let pos = marker.find(); 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(); const codeEditor = await this.getCodeEditor();
if (!codeEditor || !this.findResult) {
return;
}
const doc = codeEditor.doc; const doc = codeEditor.doc;
// //
@ -137,18 +160,23 @@ export default class FindInCode {
codeEditor.scrollIntoView(pos.from); codeEditor.scrollIntoView(pos.from);
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (totalFound > 0) { if (codeEditor && totalFound > 0) {
const doc = codeEditor.doc; const doc = codeEditor.doc;
const pos = this.findResult[currentFound].find(); const pos = this.findResult?.[currentFound].find();
// Note setting the selection sets the cursor to // Note setting the selection sets the cursor to
// the end of the selection and scrolls it into // the end of the selection and scrolls it into
// view // view
doc.setSelection(pos.from, pos.to); if (pos) {
doc.setSelection(pos.from, pos.to);
}
// Clear all markers // Clear all markers
codeEditor.operation(() => { codeEditor.operation(() => {
if (!this.findResult) {
return;
}
for (let i = 0; i < this.findResult.length; ++i) { for (let i = 0; i < this.findResult.length; ++i) {
let marker = this.findResult[i]; let marker = this.findResult[i];
marker.clear(); marker.clear();
@ -157,9 +185,9 @@ export default class FindInCode {
} }
this.findResult = null; this.findResult = null;
codeEditor.focus(); codeEditor?.focus();
} }
async replace(replaceText) { async replace(replaceText: string) {
// this.findResult may be undefined and null // this.findResult may be undefined and null
if (!this.findResult || this.findResult.length === 0) { if (!this.findResult || this.findResult.length === 0) {
return; return;
@ -178,8 +206,10 @@ export default class FindInCode {
let marker = this.findResult[currentFound]; let marker = this.findResult[currentFound];
let pos = marker.find(); let pos = marker.find();
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc; const doc = codeEditor?.doc;
doc.replaceRange(replaceText, pos.from, pos.to); if (doc) {
doc.replaceRange(replaceText, pos.from, pos.to);
}
marker.clear(); marker.clear();
let nextFound; let nextFound;
@ -194,17 +224,21 @@ export default class FindInCode {
} }
} }
} }
async replaceAll(replaceText) { async replaceAll(replaceText: string) {
if (!this.findResult || this.findResult.length === 0) { if (!this.findResult || this.findResult.length === 0) {
return; return;
} }
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc; const doc = codeEditor?.doc;
codeEditor.operation(() => { codeEditor?.operation(() => {
if (!this.findResult) {
return;
}
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) { for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound]; let marker = this.findResult[currentFound];
let pos = marker.find(); let pos = marker.find();
doc.replaceRange(replaceText, pos.from, pos.to); doc?.replaceRange(replaceText, pos.from, pos.to);
marker.clear(); marker.clear();
} }
}); });

View File

@ -4,28 +4,33 @@
import libraryLoader from "../services/library_loader.js"; import libraryLoader from "../services/library_loader.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import appContext from "../components/app_context.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_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
export default class FindInHtml { export default class FindInHtml {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private currentIndex: number;
private $results: JQuery<HTMLElement> | null;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
this.currentIndex = 0; this.currentIndex = 0;
this.$results = null; this.$results = null;
} }
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS); await libraryLoader.requireLibrary(libraryLoader.MARKJS);
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent?.noteContext?.getContentElement();
const wholeWordChar = wholeWord ? "\\b" : ""; const wholeWordChar = wholeWord ? "\\b" : "";
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
return new Promise((res) => { return new Promise((res) => {
$content.unmark({ $content?.unmark({
done: () => { done: () => {
$content.markRegExp(regExp, { $content.markRegExp(regExp, {
element: "span", element: "span",
@ -48,8 +53,8 @@ export default class FindInHtml {
}); });
} }
async findNext(direction, currentFound, nextFound) { async findNext(direction: -1 | 1, currentFound: number, nextFound: number) {
if (this.$results.length) { if (this.$results?.length) {
this.currentIndex += direction; this.currentIndex += direction;
if (this.currentIndex < 0) { if (this.currentIndex < 0) {
@ -64,13 +69,15 @@ export default class FindInHtml {
} }
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent?.noteContext?.getContentElement();
$content.unmark(); if ($content) {
$content.unmark();
}
} }
async jumpTo() { async jumpTo() {
if (this.$results.length) { if (this.$results?.length) {
const offsetTop = 100; const offsetTop = 100;
const $current = this.$results.eq(this.currentIndex); const $current = this.$results.eq(this.currentIndex);
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
@ -79,10 +86,11 @@ export default class FindInHtml {
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); $current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
const position = $current.position().top - offsetTop; const position = $current.position().top - offsetTop;
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent.noteContext?.getContentElement();
const $contentWiget = appContext.getComponentByEl($content); if ($content) {
const $contentWidget = appContext.getComponentByEl($content[0]);
$contentWiget.triggerCommand("scrollContainerTo", { position }); $contentWidget.triggerCommand("scrollContainerTo", { position });
}
} }
} }
} }

View File

@ -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 { export default class FindInText {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private findResult?: Match[] | null;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
} }
async getTextEditor() { 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 // Do this even if the searchTerm is empty so the markers are cleared and
// the counters updated // the counters updated
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
@ -54,7 +69,7 @@ export default class FindInText {
// XXX Do this accessing the private data? // XXX Do this accessing the private data?
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
for (let i = 0; i < currentFound; ++i) { 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(); const textEditor = await this.getTextEditor();
// There are no parameters for findNext/findPrev // There are no parameters for findNext/findPrev
@ -72,20 +87,23 @@ export default class FindInText {
// curFound wrap around above assumes findNext and // curFound wrap around above assumes findNext and
// findPrevious wraparound, which is what they do // findPrevious wraparound, which is what they do
if (direction > 0) { if (direction > 0) {
textEditor.execute("findNext"); textEditor?.execute("findNext");
} else { } else {
textEditor.execute("findPrevious"); textEditor?.execute("findPrevious");
} }
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
if (!textEditor) {
return;
}
if (totalFound > 0) { if (totalFound > 0) {
// Clear the markers and set the caret to the // Clear the markers and set the caret to the
// current occurrence // current occurrence
const model = textEditor.model; const model = textEditor.model;
const range = this.findResult.results.get(currentFound).marker.getRange(); const range = this.findResult?.results?.get(currentFound).marker.getRange();
// From // From
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // 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 // XXX Roll our own since already done for codeEditor and
@ -104,17 +122,17 @@ export default class FindInText {
textEditor.focus(); textEditor.focus();
} }
async replace(replaceText) { async replace(replaceText: string) {
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) { if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
const textEditor = await this.getTextEditor(); 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) { if (this.editingState !== undefined && this.editingState.results.length > 0) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
textEditor.execute("replaceAll", replaceText, this.editingState.results); textEditor?.execute("replaceAll", replaceText, this.editingState.results);
} }
} }
} }

View File

@ -5,10 +5,9 @@ import type NoteContext from "../components/note_context.js";
/** /**
* This widget allows for changing and updating depending on the active note. * This widget allows for changing and updating depending on the active note.
* @extends {BasicWidget}
*/ */
class NoteContextAwareWidget extends BasicWidget { class NoteContextAwareWidget extends BasicWidget {
protected noteContext?: NoteContext; noteContext?: NoteContext;
isNoteContext(ntxId: string | string[] | null | undefined) { isNoteContext(ntxId: string | string[] | null | undefined) {
if (Array.isArray(ntxId)) { if (Array.isArray(ntxId)) {