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 && ( +
+ {(linkType !== "external-link") && ( + <> + setLinkType(newValue as LinkType)} + /> + + )} + + {(linkType !== "reference-link" && ( +
+
+ +
+ ))} +
+ )} +
+ ); +} + +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