diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json
index ae1f438ae..99d0cea83 100644
--- a/apps/client/src/translations/cn/translation.json
+++ b/apps/client/src/translations/cn/translation.json
@@ -34,7 +34,7 @@
"link_title_mirrors": "链接标题跟随笔记标题变化",
"link_title_arbitrary": "链接标题可随意修改",
"link_title": "链接标题",
- "button_add_link": "添加链接 回车"
+ "button_add_link": "添加链接"
},
"branch_prefix": {
"edit_branch_prefix": "编辑分支前缀",
diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json
index 8adae6ca9..77024d63c 100644
--- a/apps/client/src/translations/de/translation.json
+++ b/apps/client/src/translations/de/translation.json
@@ -34,7 +34,7 @@
"link_title_mirrors": "Der Linktitel spiegelt den aktuellen Titel der Notiz wider",
"link_title_arbitrary": "Der Linktitel kann beliebig geändert werden",
"link_title": "Linktitel",
- "button_add_link": "Link hinzufügen Eingabetaste"
+ "button_add_link": "Link hinzufügen"
},
"branch_prefix": {
"edit_branch_prefix": "Zweigpräfix bearbeiten",
diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index f368095b8..2410c56b6 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -34,7 +34,7 @@
"link_title_mirrors": "link title mirrors the note's current title",
"link_title_arbitrary": "link title can be changed arbitrarily",
"link_title": "Link title",
- "button_add_link": "Add link enter"
+ "button_add_link": "Add link"
},
"branch_prefix": {
"edit_branch_prefix": "Edit branch prefix",
@@ -2007,6 +2007,7 @@
"open_externally": "Open externally"
},
"modal": {
- "close": "Close"
+ "close": "Close",
+ "help_title": "Display more information about this screen"
}
}
diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json
index aa67b4c15..9df16d534 100644
--- a/apps/client/src/translations/es/translation.json
+++ b/apps/client/src/translations/es/translation.json
@@ -34,7 +34,7 @@
"link_title_mirrors": "el título del enlace replica el título actual de la nota",
"link_title_arbitrary": "el título del enlace se puede cambiar arbitrariamente",
"link_title": "Título del enlace",
- "button_add_link": "Agregar enlace Enter"
+ "button_add_link": "Agregar enlace"
},
"branch_prefix": {
"edit_branch_prefix": "Editar prefijo de rama",
diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json
index f2303b488..f30ce130f 100644
--- a/apps/client/src/translations/fr/translation.json
+++ b/apps/client/src/translations/fr/translation.json
@@ -34,7 +34,7 @@
"link_title_mirrors": "le titre du lien reflète le titre actuel de la note",
"link_title_arbitrary": "le titre du lien peut être modifié arbitrairement",
"link_title": "Titre du lien",
- "button_add_link": "Ajouter un lien Entrée"
+ "button_add_link": "Ajouter un lien"
},
"branch_prefix": {
"edit_branch_prefix": "Modifier le préfixe de branche",
diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json
index 2e6bba53c..ee6369d85 100644
--- a/apps/client/src/translations/ro/translation.json
+++ b/apps/client/src/translations/ro/translation.json
@@ -37,7 +37,7 @@
"link_title_mirrors": "titlul legăturii corespunde titlul curent al notiței",
"note": "Notiță",
"search_note": "căutați notița după nume",
- "button_add_link": "Adaugă legătură Enter"
+ "button_add_link": "Adaugă legătură"
},
"add_relation": {
"add_relation": "Adaugă relație",
diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json
index a85495485..f9cd5595f 100644
--- a/apps/client/src/translations/tw/translation.json
+++ b/apps/client/src/translations/tw/translation.json
@@ -33,7 +33,7 @@
"link_title_mirrors": "鏈接標題跟隨筆記標題變化",
"link_title_arbitrary": "鏈接標題可隨意修改",
"link_title": "鏈接標題",
- "button_add_link": "添加鏈接 Enter"
+ "button_add_link": "添加鏈接"
},
"branch_prefix": {
"edit_branch_prefix": "編輯分支前綴",
diff --git a/apps/client/src/widgets/dialogs/add_link.ts b/apps/client/src/widgets/dialogs/add_link.ts
deleted file mode 100644
index d7758c92d..000000000
--- a/apps/client/src/widgets/dialogs/add_link.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-import { t } from "../../services/i18n.js";
-import treeService from "../../services/tree.js";
-import noteAutocompleteService from "../../services/note_autocomplete.js";
-import BasicWidget from "../basic_widget.js";
-import type { Suggestion } from "../../services/note_autocomplete.js";
-import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
-import type { EventData } from "../../components/app_context.js";
-import { openDialog } from "../../services/dialog.js";
-
-const TPL = /*html*/`
-
`;
-
-export default class AddLinkDialog extends BasicWidget {
- private $form!: JQuery;
- private $autoComplete!: JQuery;
- private $linkTitle!: JQuery;
- private $addLinkTitleSettings!: JQuery;
- private $addLinkTitleRadios!: JQuery;
- private $addLinkTitleFormGroup!: JQuery;
- private textTypeWidget: TextTypeWidget | null = null;
-
- doRender() {
- this.$widget = $(TPL);
- this.$form = this.$widget.find(".add-link-form");
- this.$autoComplete = this.$widget.find(".add-link-note-autocomplete");
- this.$linkTitle = this.$widget.find(".link-title");
- this.$addLinkTitleSettings = this.$widget.find(".add-link-title-settings");
- this.$addLinkTitleRadios = this.$widget.find(".add-link-title-radios");
- this.$addLinkTitleFormGroup = this.$widget.find(".add-link-title-form-group");
-
- this.$form.on("submit", () => {
- if (this.$autoComplete.getSelectedNotePath()) {
- this.$widget.modal("hide");
-
- const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val() as string;
-
- this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
- } else if (this.$autoComplete.getSelectedExternalLink()) {
- this.$widget.modal("hide");
-
- this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val() as string, true);
- } else {
- logError("No link to add.");
- }
-
- return false;
- });
- }
-
- async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
- this.textTypeWidget = textTypeWidget;
-
- this.$addLinkTitleSettings.toggle(!this.textTypeWidget.hasSelection());
-
- this.$addLinkTitleSettings.find("input[type=radio]").on("change", () => this.updateTitleSettingsVisibility());
-
- // with selection hyperlink is implied
- if (this.textTypeWidget.hasSelection()) {
- this.$addLinkTitleSettings.find("input[value='hyper-link']").prop("checked", true);
- } else {
- this.$addLinkTitleSettings.find("input[value='reference-link']").prop("checked", true);
- }
-
- this.updateTitleSettingsVisibility();
-
- await openDialog(this.$widget);
-
- this.$autoComplete.val("");
- this.$linkTitle.val("");
-
- const setDefaultLinkTitle = async (noteId: string) => {
- const noteTitle = await treeService.getNoteTitle(noteId);
- this.$linkTitle.val(noteTitle);
- };
-
- noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
- allowExternalLinks: true,
- allowCreatingNotes: true
- });
-
- this.$autoComplete.on("autocomplete:noteselected", (event: JQuery.Event, suggestion: Suggestion) => {
- if (!suggestion.notePath) {
- return false;
- }
-
- this.updateTitleSettingsVisibility();
-
- const noteId = treeService.getNoteIdFromUrl(suggestion.notePath);
-
- if (noteId) {
- setDefaultLinkTitle(noteId);
- }
- });
-
- this.$autoComplete.on("autocomplete:externallinkselected", (event: JQuery.Event, suggestion: Suggestion) => {
- if (!suggestion.externalLink) {
- return false;
- }
-
- this.updateTitleSettingsVisibility();
-
- this.$linkTitle.val(suggestion.externalLink);
- });
-
- this.$autoComplete.on("autocomplete:cursorchanged", (event: JQuery.Event, suggestion: Suggestion) => {
- if (suggestion.externalLink) {
- this.$linkTitle.val(suggestion.externalLink);
- } else {
- const noteId = treeService.getNoteIdFromUrl(suggestion.notePath!);
-
- if (noteId) {
- setDefaultLinkTitle(noteId);
- }
- }
- });
-
- if (text && text.trim()) {
- noteAutocompleteService.setText(this.$autoComplete, text);
- } else {
- noteAutocompleteService.showRecentNotes(this.$autoComplete);
- }
-
- this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
- }
-
- private getLinkType() {
- if (this.$autoComplete.getSelectedExternalLink()) {
- return "external-link";
- }
-
- return this.$addLinkTitleSettings.find("input[type=radio]:checked").val();
- }
-
- private updateTitleSettingsVisibility() {
- const linkType = this.getLinkType();
-
- this.$addLinkTitleFormGroup.toggle(linkType !== "reference-link");
- this.$addLinkTitleRadios.toggle(linkType !== "external-link");
- }
-}
diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx
new file mode 100644
index 000000000..d62a24304
--- /dev/null
+++ b/apps/client/src/widgets/dialogs/add_link.tsx
@@ -0,0 +1,147 @@
+import { EventData } from "../../components/app_context";
+import { closeActiveDialog, openDialog } from "../../services/dialog";
+import { t } from "../../services/i18n";
+import Modal from "../react/Modal";
+import ReactBasicWidget from "../react/ReactBasicWidget";
+import Button from "../react/Button";
+import FormRadioGroup from "../react/FormRadioGroup";
+import NoteAutocomplete from "../react/NoteAutocomplete";
+import { useState } from "preact/hooks";
+import tree from "../../services/tree";
+import { useEffect } from "react";
+import { Suggestion } from "../../services/note_autocomplete";
+import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
+import { logError } from "../../services/ws";
+
+type LinkType = "reference-link" | "external-link" | "hyper-link";
+
+interface AddLinkDialogProps {
+ text?: string;
+ textTypeWidget?: TextTypeWidget;
+}
+
+function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogProps) {
+ const [ text, setText ] = useState(_text ?? "");
+ const [ linkTitle, setLinkTitle ] = useState("");
+ const hasSelection = textTypeWidget?.hasSelection();
+ const [ linkType, setLinkType ] = useState(hasSelection ? "hyper-link" : "reference-link");
+ const [ suggestion, setSuggestion ] = useState(null);
+
+ async function setDefaultLinkTitle(noteId: string) {
+ const noteTitle = await tree.getNoteTitle(noteId);
+ setLinkTitle(noteTitle);
+ }
+
+ function resetExternalLink() {
+ if (linkType === "external-link") {
+ setLinkType("reference-link");
+ }
+ }
+
+ useEffect(() => {
+ if (!suggestion) {
+ resetExternalLink();
+ return;
+ }
+
+ if (suggestion.notePath) {
+ const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
+ if (noteId) {
+ setDefaultLinkTitle(noteId);
+ }
+ resetExternalLink();
+ }
+
+ if (suggestion.externalLink) {
+ setLinkTitle(suggestion.externalLink);
+ setLinkType("external-link");
+ }
+ }, [suggestion]);
+
+ function onSubmit() {
+ if (suggestion.notePath) {
+ // Handle note link
+ closeActiveDialog();
+ textTypeWidget?.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
+ } else if (suggestion.externalLink) {
+ // Handle external link
+ closeActiveDialog();
+ textTypeWidget?.addLink(suggestion.externalLink, linkTitle, true);
+ } else {
+ logError("No link to add.");
+ }
+ }
+
+ return (
+ }
+ onSubmit={onSubmit}
+ onHidden={() => setSuggestion(null)}
+ >
+
+
+
+
+
+
+ {!hasSelection && (
+
+ )}
+
+ );
+}
+
+export default class AddLinkDialog extends ReactBasicWidget {
+
+ private props: AddLinkDialogProps = {};
+
+ get component() {
+ return ;
+ }
+
+ async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
+ this.props.text = text;
+ this.props.textTypeWidget = textTypeWidget;
+ this.doRender();
+ await openDialog(this.$widget);
+ }
+
+}
diff --git a/apps/client/src/widgets/react/Button.tsx b/apps/client/src/widgets/react/Button.tsx
index 528b3a696..d6ba0f7d3 100644
--- a/apps/client/src/widgets/react/Button.tsx
+++ b/apps/client/src/widgets/react/Button.tsx
@@ -3,7 +3,7 @@ import { useRef } from "preact/hooks";
interface ButtonProps {
/** Reference to the button element. Mostly useful for requesting focus. */
- buttonRef: RefObject;
+ buttonRef?: RefObject;
text: string;
className?: string;
keyboardShortcut?: string;
diff --git a/apps/client/src/widgets/react/Modal.tsx b/apps/client/src/widgets/react/Modal.tsx
index cf3825e7c..43e6b12ee 100644
--- a/apps/client/src/widgets/react/Modal.tsx
+++ b/apps/client/src/widgets/react/Modal.tsx
@@ -37,7 +37,7 @@ export default function Modal({ children, className, size, title, footer, onShow
}
}
});
- }
+ }
const style: CSSProperties = {};
if (maxWidth) {
@@ -51,7 +51,7 @@ export default function Modal({ children, className, size, title, footer, onShow
{title}
{helpPageId && (
-
+
)}
diff --git a/apps/client/src/widgets/react/NoteAutocomplete.tsx b/apps/client/src/widgets/react/NoteAutocomplete.tsx
new file mode 100644
index 000000000..36702b96e
--- /dev/null
+++ b/apps/client/src/widgets/react/NoteAutocomplete.tsx
@@ -0,0 +1,48 @@
+import { useRef } from "preact/hooks";
+import { t } from "../../services/i18n";
+import { use, useEffect } from "react";
+import note_autocomplete, { type Suggestion } from "../../services/note_autocomplete";
+
+interface NoteAutocompleteProps {
+ text?: string;
+ allowExternalLinks?: boolean;
+ allowCreatingNotes?: boolean;
+ onChange?: (suggestion: Suggestion) => void;
+}
+
+export default function NoteAutocomplete({ text, allowCreatingNotes, allowExternalLinks, onChange }: NoteAutocompleteProps) {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (!ref.current) return;
+ const $autoComplete = $(ref.current);
+
+ note_autocomplete.initNoteAutocomplete($autoComplete, {
+ allowExternalLinks,
+ allowCreatingNotes
+ });
+ if (onChange) {
+ $autoComplete.on("autocomplete:noteselected", (_e, suggestion) => onChange(suggestion));
+ $autoComplete.on("autocomplete:externallinkselected", (_e, suggestion) => onChange(suggestion));
+ }
+ }, [allowExternalLinks, allowCreatingNotes]);
+
+ useEffect(() => {
+ if (!ref.current) return;
+ if (text) {
+ const $autoComplete = $(ref.current);
+ note_autocomplete.setText($autoComplete, text);
+ } else {
+ ref.current.value = "";
+ }
+ }, [text]);
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file