chore(client/ts): port highlights_list

This commit is contained in:
Elian Doran 2025-01-07 12:34:10 +02:00
parent 0713b4aec8
commit 85c225fe05
No known key found for this signature in database
9 changed files with 133 additions and 66 deletions

View File

@ -208,6 +208,29 @@ type EventMappings = {
/** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */ /** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */
activeScreenChanged: { activeScreenChanged: {
activeScreen: Screen; activeScreen: Screen;
},
activeContextChanged: {
noteContext: NoteContext;
},
noteSwitched: {
noteContext: NoteContext;
notePath: string;
},
noteSwitchedAndActivatedEvent: {
noteContext: NoteContext;
notePath: string;
},
setNoteContext: {
noteContext: NoteContext;
},
noteTypeMimeChangedEvent: {
noteId: string;
},
reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined;
},
showHighlightsListWidget: {
noteId: string;
} }
} }

View File

@ -306,7 +306,7 @@ class NoteContext extends Component
} }
async getTextEditor(callback?: GetTextEditorCallback) { async getTextEditor(callback?: GetTextEditorCallback) {
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', { return this.timeout<TextEditor>(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
callback, callback,
resolve, resolve,
ntxId: this.ntxId ntxId: this.ntxId
@ -321,7 +321,7 @@ class NoteContext extends Component
} }
async getContentElement() { async getContentElement() {
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithContentElement', { return this.timeout<JQuery<HTMLElement>>(new Promise(resolve => appContext.triggerCommand('executeWithContentElement', {
resolve, resolve,
ntxId: this.ntxId ntxId: this.ntxId
}))); })));
@ -334,11 +334,11 @@ class NoteContext extends Component
}))); })));
} }
timeout(promise: Promise<unknown>) { timeout<T>(promise: Promise<T | null>) {
return Promise.race([ return Promise.race([
promise, promise,
new Promise(res => setTimeout(() => res(null), 200)) new Promise(res => setTimeout(() => res(null), 200))
]); ]) as Promise<T>;
} }
resetViewScope() { resetViewScope() {

View File

@ -30,7 +30,7 @@ async function removeAttributeById(noteId: string, attributeId: string) {
* 2. attribute is owned by the template of the note * 2. attribute is owned by the template of the note
* 3. attribute is owned by some note's ancestor and is inheritable * 3. attribute is owned by some note's ancestor and is inheritable
*/ */
function isAffecting(attrRow: AttributeRow, affectedNote: FNote) { function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefined) {
if (!affectedNote || !attrRow) { if (!affectedNote || !attrRow) {
return false; return false;
} }

View File

@ -31,6 +31,8 @@ export interface ViewScope {
viewMode?: ViewMode; viewMode?: ViewMode;
attachmentId?: string; attachmentId?: string;
readOnlyTemporarilyDisabled?: boolean; readOnlyTemporarilyDisabled?: boolean;
highlightsListPreviousVisible?: boolean;
highlightsListTemporarilyHidden?: boolean;
} }
interface CreateLinkOptions { interface CreateLinkOptions {

View File

@ -172,7 +172,7 @@ export default class LoadResults {
this.contentNoteIdToComponentId.push({noteId, componentId}); this.contentNoteIdToComponentId.push({noteId, componentId});
} }
isNoteContentReloaded(noteId: string, componentId: string) { isNoteContentReloaded(noteId: string, componentId?: string) {
if (!noteId) { if (!noteId) {
return false; return false;
} }

View File

@ -16,14 +16,14 @@ class Options {
} }
get(key: string) { get(key: string) {
return this.arr?.[key]; return this.arr?.[key] as string;
} }
getNames() { getNames() {
return Object.keys(this.arr || []); return Object.keys(this.arr || []);
} }
getJson(key: string) { getJson(key: string) {
const value = this.arr?.[key]; const value = this.arr?.[key];
if (typeof value !== "string") { if (typeof value !== "string") {
return null; return null;
@ -76,4 +76,4 @@ class Options {
const options = new Options(); const options = new Options();
export default options; export default options;

View File

@ -176,6 +176,12 @@ declare global {
} }
}; };
var katex: {
renderToString(text: string, opts: {
throwOnError: boolean
});
}
type TextEditorElement = {}; type TextEditorElement = {};
interface Writer { interface Writer {
setAttribute(name: string, value: string, el: TextEditorElement); setAttribute(name: string, value: string, el: TextEditorElement);
@ -220,6 +226,13 @@ declare global {
}); });
getRoot(): TextEditorElement getRoot(): TextEditorElement
}, },
domRoots: {
values: () => {
next: () => {
value: string;
}
};
}
change(cb: (writer: Writer) => void) change(cb: (writer: Writer) => void)
} }
}, },

View File

@ -10,22 +10,23 @@ import attributeService from "../services/attributes.js";
import RightPanelWidget from "./right_panel_widget.js"; import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js"; import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js"; import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext from "../components/app_context.js"; import appContext, { EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js"; import libraryLoader from "../services/library_loader.js";
import FNote from "../entities/fnote.js";
const TPL = `<div class="highlights-list-widget"> const TPL = `<div class="highlights-list-widget">
<style> <style>
.highlights-list-widget { .highlights-list-widget {
padding: 10px; padding: 10px;
contain: none; contain: none;
overflow: auto; overflow: auto;
position: relative; position: relative;
} }
.highlights-list > ol { .highlights-list > ol {
padding-left: 20px; padding-left: 20px;
} }
.highlights-list li { .highlights-list li {
cursor: pointer; cursor: pointer;
margin-bottom: 3px; margin-bottom: 3px;
@ -33,7 +34,7 @@ const TPL = `<div class="highlights-list-widget">
word-wrap: break-word; word-wrap: break-word;
hyphens: auto; hyphens: auto;
} }
.highlights-list li:hover { .highlights-list li:hover {
font-weight: bold; font-weight: bold;
} }
@ -43,6 +44,9 @@ const TPL = `<div class="highlights-list-widget">
</div>`; </div>`;
export default class HighlightsListWidget extends RightPanelWidget { export default class HighlightsListWidget extends RightPanelWidget {
private $highlightsList!: JQuery<HTMLElement>;
get widgetTitle() { get widgetTitle() {
return t("highlights_list_2.title"); return t("highlights_list_2.title");
} }
@ -58,16 +62,16 @@ export default class HighlightsListWidget extends RightPanelWidget {
new OnClickButtonWidget() new OnClickButtonWidget()
.icon("bx-x") .icon("bx-x")
.titlePlacement("left") .titlePlacement("left")
.onClick(widget => widget.triggerCommand("closeHlt")) .onClick((widget: OnClickButtonWidget) => widget.triggerCommand("closeHlt"))
.class("icon-action") .class("icon-action")
]; ];
} }
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled()
&& this.note.type === 'text' && this.note != null && this.note.type === 'text'
&& !this.noteContext.viewScope.highlightsListTemporarilyHidden && !this.noteContext?.viewScope?.highlightsListTemporarilyHidden
&& this.noteContext.viewScope.viewMode === 'default'; && this.noteContext?.viewScope?.viewMode === 'default';
} }
async doRenderBody() { async doRenderBody() {
@ -75,13 +79,13 @@ export default class HighlightsListWidget extends RightPanelWidget {
this.$highlightsList = this.$body.find('.highlights-list'); this.$highlightsList = this.$body.find('.highlights-list');
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote | null | undefined) {
/* The reason for adding highlightsListPreviousVisible is to record whether the previous state /* The reason for adding highlightsListPreviousVisible is to record whether the previous state
of the highlightsList is hidden or displayed, and then let it be displayed/hidden at the initial time. of the highlightsList is hidden or displayed, and then let it be displayed/hidden at the initial time.
If there is no such value, when the right panel needs to display toc but not highlighttext, If there is no such value, when the right panel needs to display toc but not highlighttext,
every time the note content is changed, highlighttext Widget will appear and then close immediately, every time the note content is changed, highlighttext Widget will appear and then close immediately,
because getHlt function will consume time */ because getHlt function will consume time */
if (this.noteContext.viewScope.highlightsListPreviousVisible) { if (this.noteContext?.viewScope?.highlightsListPreviousVisible) {
this.toggleInt(true); this.toggleInt(true);
} else { } else {
this.toggleInt(false); this.toggleInt(false);
@ -89,31 +93,41 @@ export default class HighlightsListWidget extends RightPanelWidget {
const optionsHighlightsList = JSON.parse(options.get('highlightsList')); const optionsHighlightsList = JSON.parse(options.get('highlightsList'));
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHighlightsList.length) { if (note?.isLabelTruthy('hideHighlightWidget') || !optionsHighlightsList.length) {
this.toggleInt(false); this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
return; return;
} }
let $highlightsList = "", hlLiCount = -1; let $highlightsList: JQuery<HTMLElement> | null = null;
let hlLiCount = -1;
// Check for type text unconditionally in case alwaysShowWidget is set // Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') { if (note && this.note?.type === 'text') {
const { content } = await note.getNoteComplement(); const noteComplement = await note.getNoteComplement();
({ $highlightsList, hlLiCount } = await this.getHighlightList(content, optionsHighlightsList)); if (noteComplement && "content" in noteComplement) {
({ $highlightsList, hlLiCount } = await this.getHighlightList(noteComplement.content, optionsHighlightsList));
}
}
this.$highlightsList.empty();
if ($highlightsList) {
this.$highlightsList.append($highlightsList);
} }
this.$highlightsList.empty().append($highlightsList);
if (hlLiCount > 0) { if (hlLiCount > 0) {
this.toggleInt(true); this.toggleInt(true);
this.noteContext.viewScope.highlightsListPreviousVisible = true; if (this.noteContext?.viewScope) {
this.noteContext.viewScope.highlightsListPreviousVisible = true;
}
} else { } else {
this.toggleInt(false); this.toggleInt(false);
this.noteContext.viewScope.highlightsListPreviousVisible = false; if (this.noteContext?.viewScope) {
this.noteContext.viewScope.highlightsListPreviousVisible = false;
}
} }
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
} }
extractOuterTag(htmlStr) { extractOuterTag(htmlStr: string | null) {
if (htmlStr === null) { if (htmlStr === null) {
return null return null
} }
@ -128,7 +142,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
return null; return null;
} }
areOuterTagsConsistent(str1, str2) { areOuterTagsConsistent(str1: string | null, str2: string | null) {
const tag1 = this.extractOuterTag(str1); const tag1 = this.extractOuterTag(str1);
const tag2 = this.extractOuterTag(str2); const tag2 = this.extractOuterTag(str2);
// If one of them has no label, returns false // If one of them has no label, returns false
@ -142,10 +156,10 @@ export default class HighlightsListWidget extends RightPanelWidget {
/** /**
* Rendering formulas in strings using katex * Rendering formulas in strings using katex
* *
* @param {string} html Note's html content * @param html Note's html content
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX. * @returns The HTML content with mathematical formulas rendered by KaTeX.
*/ */
async replaceMathTextWithKatax(html) { async replaceMathTextWithKatax(html: string) {
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g; const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
var matches = [...html.matchAll(mathTextRegex)]; var matches = [...html.matchAll(mathTextRegex)];
let modifiedText = html; let modifiedText = html;
@ -185,7 +199,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
return modifiedText; return modifiedText;
} }
async getHighlightList(content, optionsHighlightsList) { async getHighlightList(content: string, optionsHighlightsList: string[]) {
// matches a span containing background-color // matches a span containing background-color
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi; const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
// matches a span containing color // matches a span containing color
@ -225,7 +239,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
const combinedRegex = new RegExp(combinedRegexStr, 'gi'); const combinedRegex = new RegExp(combinedRegexStr, 'gi');
const $highlightsList = $("<ol>"); const $highlightsList = $("<ol>");
let prevEndIndex = -1, hlLiCount = 0; let prevEndIndex = -1, hlLiCount = 0;
let prevSubHtml = null; let prevSubHtml: string | null = null;
// Used to determine if a string is only a formula // Used to determine if a string is only a formula
const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/; const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/;
@ -272,7 +286,11 @@ export default class HighlightsListWidget extends RightPanelWidget {
}; };
} }
async jumpToHighlightsList(findSubStr, itemIndex) { async jumpToHighlightsList(findSubStr: string, itemIndex: number) {
if (!this.noteContext) {
return;
}
const isReadOnly = await this.noteContext.isReadOnly(); const isReadOnly = await this.noteContext.isReadOnly();
let targetElement; let targetElement;
if (isReadOnly) { if (isReadOnly) {
@ -280,59 +298,70 @@ export default class HighlightsListWidget extends RightPanelWidget {
targetElement = $container.find(findSubStr).filter(function () { targetElement = $container.find(findSubStr).filter(function () {
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) { if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
let color = this.style.color; let color = this.style.color;
return !($(this).prop('tagName') === "SPAN" && color === ""); const $el = $(this as HTMLElement);
return !($el.prop('tagName') === "SPAN" && color === "");
} else { } else {
return true; return true;
} }
}).filter(function () { }).filter(function () {
return $(this).parent(findSubStr).length === 0 const $el = $(this as HTMLElement);
&& $(this).parent().parent(findSubStr).length === 0 return $el.parent(findSubStr).length === 0
&& $(this).parent().parent().parent(findSubStr).length === 0 && $el.parent().parent(findSubStr).length === 0
&& $(this).parent().parent().parent().parent(findSubStr).length === 0; && $el.parent().parent().parent(findSubStr).length === 0
&& $el.parent().parent().parent().parent(findSubStr).length === 0;
}) })
} else { } else {
const textEditor = await this.noteContext.getTextEditor(); const textEditor = await this.noteContext.getTextEditor();
targetElement = $(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function () { if (textEditor) {
// When finding span[style*="color"] but not looking for span[style*="background-color"], targetElement = $(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function () {
// the background-color error will be regarded as color, so it needs to be filtered // When finding span[style*="color"] but not looking for span[style*="background-color"],
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) { // the background-color error will be regarded as color, so it needs to be filtered
let color = this.style.color; const $el = $(this as HTMLElement);
return !($(this).prop('tagName') === "SPAN" && color === ""); if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
} else { let color = this.style.color;
return true; return !($el.prop('tagName') === "SPAN" && color === "");
} } else {
}).filter(function () { return true;
// Need to filter out the child elements of the element that has been found }
return $(this).parent(findSubStr).length === 0 }).filter(function () {
&& $(this).parent().parent(findSubStr).length === 0 // Need to filter out the child elements of the element that has been found
&& $(this).parent().parent().parent(findSubStr).length === 0 const $el = $(this as HTMLElement);
&& $(this).parent().parent().parent().parent(findSubStr).length === 0; return $el.parent(findSubStr).length === 0
}) && $el.parent().parent(findSubStr).length === 0
&& $el.parent().parent().parent(findSubStr).length === 0
&& $el.parent().parent().parent().parent(findSubStr).length === 0;
})
}
}
if (targetElement) {
targetElement[itemIndex].scrollIntoView({
behavior: "smooth", block: "center"
});
} }
targetElement[itemIndex].scrollIntoView({
behavior: "smooth", block: "center"
});
} }
async closeHltCommand() { async closeHltCommand() {
this.noteContext.viewScope.highlightsListTemporarilyHidden = true; if (this.noteContext?.viewScope) {
this.noteContext.viewScope.highlightsListTemporarilyHidden = true;
}
await this.refresh(); await this.refresh();
this.triggerCommand('reEvaluateRightPaneVisibility'); this.triggerCommand('reEvaluateRightPaneVisibility');
appContext.triggerEvent("reEvaluateHighlightsListWidgetVisibility", { noteId: this.noteId }); appContext.triggerEvent("reEvaluateHighlightsListWidgetVisibility", { noteId: this.noteId });
} }
async showHighlightsListWidgetEvent({ noteId }) { async showHighlightsListWidgetEvent({ noteId }: EventData<"showHighlightsListWidget">) {
if (this.noteId === noteId) { if (this.noteId === noteId) {
await this.refresh(); await this.refresh();
this.triggerCommand('reEvaluateRightPaneVisibility'); this.triggerCommand('reEvaluateRightPaneVisibility');
} }
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteContentReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh(); await this.refresh();
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label' } else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget') && (attr.name?.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
&& attributeService.isAffecting(attr, this.note))) { && attributeService.isAffecting(attr, this.note))) {
await this.refresh(); await this.refresh();
} }

View File

@ -9,7 +9,7 @@ import NoteContext from "../components/note_context.js";
*/ */
class NoteContextAwareWidget extends BasicWidget { class NoteContextAwareWidget extends BasicWidget {
private noteContext?: NoteContext; protected noteContext?: NoteContext;
isNoteContext(ntxId: string) { isNoteContext(ntxId: string) {
if (Array.isArray(ntxId)) { if (Array.isArray(ntxId)) {