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;
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;
}

View File

@ -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();
}
});

View File

@ -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<HTMLElement> | 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 });
}
}
}
}

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 {
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);
}
}
}

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.
* @extends {BasicWidget}
*/
class NoteContextAwareWidget extends BasicWidget {
protected noteContext?: NoteContext;
noteContext?: NoteContext;
isNoteContext(ntxId: string | string[] | null | undefined) {
if (Array.isArray(ntxId)) {