mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 15:55:52 +01:00
chore(react/type_widgets): port text link insertion mechanism
This commit is contained in:
@@ -33,6 +33,7 @@ import { ColumnComponent } from "tabulator-tables";
|
|||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||||
|
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@@ -223,7 +224,7 @@ export type CommandMappings = {
|
|||||||
showProtectedSessionPasswordDialog: CommandData;
|
showProtectedSessionPasswordDialog: CommandData;
|
||||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||||
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||||
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
|
showAddLinkDialog: CommandData & AddLinkOpts;
|
||||||
closeProtectedSessionPasswordDialog: CommandData;
|
closeProtectedSessionPasswordDialog: CommandData;
|
||||||
copyImageReferenceToClipboard: CommandData;
|
copyImageReferenceToClipboard: CommandData;
|
||||||
copyImageToClipboard: CommandData;
|
copyImageToClipboard: CommandData;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import NoteAutocomplete from "../react/NoteAutocomplete";
|
|||||||
import { useRef, useState, useEffect } from "preact/hooks";
|
import { useRef, useState, useEffect } from "preact/hooks";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
|
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
|
||||||
import { default as TextTypeWidget } from "../type_widgets_old/editable_text.js";
|
|
||||||
import { logError } from "../../services/ws";
|
import { logError } from "../../services/ws";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
@@ -14,28 +13,31 @@ import { useTriliumEvent } from "../react/hooks";
|
|||||||
|
|
||||||
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
type LinkType = "reference-link" | "external-link" | "hyper-link";
|
||||||
|
|
||||||
|
export interface AddLinkOpts {
|
||||||
|
text: string;
|
||||||
|
hasSelection: boolean;
|
||||||
|
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AddLinkDialog() {
|
export default function AddLinkDialog() {
|
||||||
const [ textTypeWidget, setTextTypeWidget ] = useState<TextTypeWidget>();
|
const [ opts, setOpts ] = useState<AddLinkOpts>();
|
||||||
const initialText = useRef<string>();
|
|
||||||
const [ linkTitle, setLinkTitle ] = useState("");
|
const [ linkTitle, setLinkTitle ] = useState("");
|
||||||
const hasSelection = textTypeWidget?.hasSelection();
|
const [ linkType, setLinkType ] = useState<LinkType>();
|
||||||
const [ linkType, setLinkType ] = useState<LinkType>(hasSelection ? "hyper-link" : "reference-link");
|
|
||||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
useTriliumEvent("showAddLinkDialog", ( { textTypeWidget, text }) => {
|
useTriliumEvent("showAddLinkDialog", opts => {
|
||||||
setTextTypeWidget(textTypeWidget);
|
setOpts(opts);
|
||||||
initialText.current = text;
|
|
||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSelection) {
|
if (opts?.hasSelection) {
|
||||||
setLinkType("hyper-link");
|
setLinkType("hyper-link");
|
||||||
} else {
|
} else {
|
||||||
setLinkType("reference-link");
|
setLinkType("reference-link");
|
||||||
}
|
}
|
||||||
}, [ hasSelection ])
|
}, [ opts ])
|
||||||
|
|
||||||
async function setDefaultLinkTitle(noteId: string) {
|
async function setDefaultLinkTitle(noteId: string) {
|
||||||
const noteTitle = await tree.getNoteTitle(noteId);
|
const noteTitle = await tree.getNoteTitle(noteId);
|
||||||
@@ -70,10 +72,10 @@ export default function AddLinkDialog() {
|
|||||||
|
|
||||||
function onShown() {
|
function onShown() {
|
||||||
const $autocompleteEl = refToJQuerySelector(autocompleteRef);
|
const $autocompleteEl = refToJQuerySelector(autocompleteRef);
|
||||||
if (!initialText.current) {
|
if (!opts?.text) {
|
||||||
note_autocomplete.showRecentNotes($autocompleteEl);
|
note_autocomplete.showRecentNotes($autocompleteEl);
|
||||||
} else {
|
} else {
|
||||||
note_autocomplete.setText($autocompleteEl, initialText.current);
|
note_autocomplete.setText($autocompleteEl, opts.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// to be able to quickly remove entered text
|
// to be able to quickly remove entered text
|
||||||
@@ -86,11 +88,11 @@ export default function AddLinkDialog() {
|
|||||||
if (suggestion?.notePath) {
|
if (suggestion?.notePath) {
|
||||||
// Handle note link
|
// Handle note link
|
||||||
setShown(false);
|
setShown(false);
|
||||||
textTypeWidget?.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
|
opts?.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
|
||||||
} else if (suggestion?.externalLink) {
|
} else if (suggestion?.externalLink) {
|
||||||
// Handle external link
|
// Handle external link
|
||||||
setShown(false);
|
setShown(false);
|
||||||
textTypeWidget?.addLink(suggestion.externalLink, linkTitle, true);
|
opts?.addLink(suggestion.externalLink, linkTitle, true);
|
||||||
} else {
|
} else {
|
||||||
logError("No link to add.");
|
logError("No link to add.");
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ export default function AddLinkDialog() {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{!hasSelection && (
|
{!opts?.hasSelection && (
|
||||||
<div className="add-link-title-settings">
|
<div className="add-link-title-settings">
|
||||||
{(linkType !== "external-link") && (
|
{(linkType !== "external-link") && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { HTMLProps, RefObject, useEffect, useRef, useState } from "preact/compat";
|
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
|
||||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor } from "@triliumnext/ckeditor5";
|
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import { buildConfig, BuildEditorOptions } from "./config";
|
import { buildConfig, BuildEditorOptions } from "./config";
|
||||||
|
import { useLegacyImperativeHandlers } from "../../react/hooks";
|
||||||
|
import link from "../../../services/link";
|
||||||
|
|
||||||
|
export interface CKEditorApi {
|
||||||
|
/** returns true if user selected some text, false if there's no selection */
|
||||||
|
hasSelection(): boolean;
|
||||||
|
getSelectedText(): string;
|
||||||
|
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): void;
|
||||||
|
addLinkToEditor(linkHref: string, linkTitle: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "className" | "tabIndex"> {
|
interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "className" | "tabIndex"> {
|
||||||
content: string | undefined;
|
content: string | undefined;
|
||||||
@@ -13,13 +23,69 @@ interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "cla
|
|||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
/** Called upon whenever a new CKEditor instance is initialized, whether it's the first initialization, after a crash or after a config change that requires it (e.g. content language). */
|
/** Called upon whenever a new CKEditor instance is initialized, whether it's the first initialization, after a crash or after a config change that requires it (e.g. content language). */
|
||||||
onEditorInitialized?: (editor: CKTextEditor) => void;
|
onEditorInitialized?: (editor: CKTextEditor) => void;
|
||||||
|
editorApi: RefObject<CKEditorApi>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CKEditorWithWatchdog({ content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized }: CKEditorWithWatchdogProps) {
|
export default function CKEditorWithWatchdog({ content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi }: CKEditorWithWatchdogProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const watchdogRef = useRef<EditorWatchdog>(null);
|
const watchdogRef = useRef<EditorWatchdog>(null);
|
||||||
const [ editor, setEditor ] = useState<CKTextEditor>();
|
const [ editor, setEditor ] = useState<CKTextEditor>();
|
||||||
|
|
||||||
|
useImperativeHandle(editorApi, () => ({
|
||||||
|
hasSelection() {
|
||||||
|
const model = watchdogRef.current?.editor?.model;
|
||||||
|
const selection = model?.document.selection;
|
||||||
|
|
||||||
|
return !selection?.isCollapsed;
|
||||||
|
},
|
||||||
|
getSelectedText() {
|
||||||
|
const range = watchdogRef.current?.editor?.model.document.selection.getFirstRange();
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of range.getItems()) {
|
||||||
|
if ("data" in item && item.data) {
|
||||||
|
text += item.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
addLink(notePath, linkTitle, externalLink) {
|
||||||
|
const editor = watchdogRef.current?.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
|
||||||
|
if (linkTitle) {
|
||||||
|
if (this.hasSelection()) {
|
||||||
|
editor.execute("link", externalLink ? `${notePath}` : `#${notePath}`);
|
||||||
|
} else {
|
||||||
|
this.addLinkToEditor(externalLink ? `${notePath}` : `#${notePath}`, linkTitle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editor.execute("referenceLink", { href: "#" + notePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.editing.view.focus();
|
||||||
|
},
|
||||||
|
addLinkToEditor(linkHref, linkTitle) {
|
||||||
|
watchdogRef.current?.editor?.model.change((writer) => {
|
||||||
|
const insertPosition = watchdogRef.current?.editor?.model.document.selection.getFirstPosition();
|
||||||
|
if (insertPosition) {
|
||||||
|
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
useLegacyImperativeHandlers({
|
||||||
|
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) {
|
||||||
|
await link.loadReferenceLinkTitle($el, href);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import toast from "../../../services/toast";
|
|||||||
import utils, { deferred, isMobile } from "../../../services/utils";
|
import utils, { deferred, isMobile } from "../../../services/utils";
|
||||||
import { useEditorSpacedUpdate, useKeyboardShortcuts, useNoteLabel, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
import { useEditorSpacedUpdate, useKeyboardShortcuts, useNoteLabel, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
||||||
import { TypeWidgetProps } from "../type_widget";
|
import { TypeWidgetProps } from "../type_widget";
|
||||||
import CKEditorWithWatchdog from "./CKEditorWithWatchdog";
|
import CKEditorWithWatchdog, { CKEditorApi } from "./CKEditorWithWatchdog";
|
||||||
import "./EditableText.css";
|
import "./EditableText.css";
|
||||||
import { CKTextEditor, ClassicEditor, EditorWatchdog } from "@triliumnext/ckeditor5";
|
import { CKTextEditor, ClassicEditor, EditorWatchdog } from "@triliumnext/ckeditor5";
|
||||||
import Component from "../../../components/component";
|
import Component from "../../../components/component";
|
||||||
import { RefObject } from "preact";
|
|
||||||
import options from "../../../services/options";
|
import options from "../../../services/options";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +20,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [ content, setContent ] = useState<string>();
|
const [ content, setContent ] = useState<string>();
|
||||||
const watchdogRef = useRef<EditorWatchdog>(null);
|
const watchdogRef = useRef<EditorWatchdog>(null);
|
||||||
|
const editorApiRef = useRef<CKEditorApi>(null);
|
||||||
const [ language ] = useNoteLabel(note, "language");
|
const [ language ] = useNoteLabel(note, "language");
|
||||||
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
|
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
|
||||||
const isClassicEditor = isMobile() || textNoteEditorType === "ckeditor-classic";
|
const isClassicEditor = isMobile() || textNoteEditorType === "ckeditor-classic";
|
||||||
@@ -66,6 +66,18 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
editor?.editing.view.focus();
|
editor?.editing.view.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useTriliumEvent("addLinkToText", ({ ntxId: eventNtxId }) => {
|
||||||
|
if (eventNtxId !== ntxId || !editorApiRef.current) return;
|
||||||
|
parentComponent?.triggerCommand("showAddLinkDialog", {
|
||||||
|
text: editorApiRef.current.getSelectedText(),
|
||||||
|
hasSelection: editorApiRef.current.hasSelection(),
|
||||||
|
async addLink(notePath, linkTitle, externalLink) {
|
||||||
|
await waitForEditor();
|
||||||
|
return editorApiRef.current?.addLink(notePath, linkTitle, externalLink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function waitForEditor() {
|
async function waitForEditor() {
|
||||||
await initialized.current;
|
await initialized.current;
|
||||||
const editor = watchdogRef.current?.editor;
|
const editor = watchdogRef.current?.editor;
|
||||||
@@ -104,6 +116,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
content={content}
|
content={content}
|
||||||
contentLanguage={language}
|
contentLanguage={language}
|
||||||
isClassicEditor={isClassicEditor}
|
isClassicEditor={isClassicEditor}
|
||||||
|
editorApi={editorApiRef}
|
||||||
watchdogRef={watchdogRef}
|
watchdogRef={watchdogRef}
|
||||||
watchdogConfig={{
|
watchdogConfig={{
|
||||||
// An average number of milliseconds between the last editor errors (defaults to 5000). When the period of time between errors is lower than that and the crashNumberLimit is also reached, the watchdog changes its state to crashedPermanently, and it stops restarting the editor. This prevents an infinite restart loop.
|
// An average number of milliseconds between the last editor errors (defaults to 5000). When the period of time between errors is lower than that and the crashNumberLimit is also reached, the watchdog changes its state to crashedPermanently, and it stops restarting the editor. This prevents an infinite restart loop.
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
this.refreshCodeBlockOptions();
|
this.refreshCodeBlockOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) {
|
|
||||||
await linkService.loadReferenceLinkTitle($el, href);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCodeBlockOptions() {
|
refreshCodeBlockOptions() {
|
||||||
const wordWrap = options.is("codeBlockWordWrap");
|
const wordWrap = options.is("codeBlockWordWrap");
|
||||||
this.$widget.toggleClass("word-wrap", wordWrap);
|
this.$widget.toggleClass("word-wrap", wordWrap);
|
||||||
|
|||||||
@@ -55,39 +55,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
return this.watchdog?.editor;
|
return this.watchdog?.editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLinkToEditor(linkHref: string, linkTitle: string) {
|
|
||||||
await this.initialized;
|
|
||||||
|
|
||||||
this.watchdog.editor?.model.change((writer) => {
|
|
||||||
const insertPosition = this.watchdog.editor?.model.document.selection.getFirstPosition();
|
|
||||||
if (insertPosition) {
|
|
||||||
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async addLink(notePath: string, linkTitle: string | null, externalLink: boolean = false) {
|
|
||||||
await this.initialized;
|
|
||||||
|
|
||||||
if (linkTitle) {
|
|
||||||
if (this.hasSelection()) {
|
|
||||||
this.watchdog.editor?.execute("link", externalLink ? `${notePath}` : `#${notePath}`);
|
|
||||||
} else {
|
|
||||||
await this.addLinkToEditor(externalLink ? `${notePath}` : `#${notePath}`, linkTitle);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.watchdog.editor?.execute("referenceLink", { href: "#" + notePath });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.watchdog.editor?.editing.view.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if user selected some text, false if there's no selection
|
|
||||||
hasSelection() {
|
|
||||||
const model = this.watchdog.editor?.model;
|
|
||||||
const selection = model?.document.selection;
|
|
||||||
|
|
||||||
return !selection?.isCollapsed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
|
async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
|
||||||
@@ -108,29 +75,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
resolve(this.watchdog.editor as CKTextEditor);
|
resolve(this.watchdog.editor as CKTextEditor);
|
||||||
}
|
}
|
||||||
|
|
||||||
addLinkToTextCommand() {
|
|
||||||
const selectedText = this.getSelectedText();
|
|
||||||
|
|
||||||
this.triggerCommand("showAddLinkDialog", { textTypeWidget: this, text: selectedText });
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedText() {
|
|
||||||
const range = this.watchdog.editor?.model.document.selection.getFirstRange();
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
if (!range) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of range.getItems()) {
|
|
||||||
if ("data" in item && item.data) {
|
|
||||||
text += item.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
async followLinkUnderCursorCommand() {
|
async followLinkUnderCursorCommand() {
|
||||||
await this.initialized;
|
await this.initialized;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user