mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react): port add_link
This commit is contained in:
parent
a62f12b427
commit
e619a6ef7c
@ -34,7 +34,7 @@
|
||||
"link_title_mirrors": "链接标题跟随笔记标题变化",
|
||||
"link_title_arbitrary": "链接标题可随意修改",
|
||||
"link_title": "链接标题",
|
||||
"button_add_link": "添加链接 <kbd>回车</kbd>"
|
||||
"button_add_link": "添加链接"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "编辑分支前缀",
|
||||
|
@ -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 <kbd>Eingabetaste</kbd>"
|
||||
"button_add_link": "Link hinzufügen"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Zweigpräfix bearbeiten",
|
||||
|
@ -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 <kbd>enter</kbd>"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -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 <kbd>Enter</kbd>"
|
||||
"button_add_link": "Agregar enlace"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Editar prefijo de rama",
|
||||
|
@ -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 <kbd>Entrée</kbd>"
|
||||
"button_add_link": "Ajouter un lien"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Modifier le préfixe de branche",
|
||||
|
@ -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ă <kbd>Enter</kbd>"
|
||||
"button_add_link": "Adaugă legătură"
|
||||
},
|
||||
"add_relation": {
|
||||
"add_relation": "Adaugă relație",
|
||||
|
@ -33,7 +33,7 @@
|
||||
"link_title_mirrors": "鏈接標題跟隨筆記標題變化",
|
||||
"link_title_arbitrary": "鏈接標題可隨意修改",
|
||||
"link_title": "鏈接標題",
|
||||
"button_add_link": "添加鏈接 <kbd>Enter</kbd>"
|
||||
"button_add_link": "添加鏈接"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "編輯分支前綴",
|
||||
|
@ -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*/`
|
||||
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 1000px" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title flex-grow-1">${t("add_link.add_link")}</h5>
|
||||
<button type="button" class="help-button" title="${t("add_link.help_on_links")}" data-help-page="links.html">?</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("add_link.close")}"></button>
|
||||
</div>
|
||||
<form class="add-link-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="add-link-note-autocomplete">${t("add_link.note")}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input class="add-link-note-autocomplete form-control" placeholder="${t("add_link.search_note")}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-link-title-settings">
|
||||
<div class="add-link-title-radios form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="link-type" value="reference-link" checked>
|
||||
${t("add_link.link_title_mirrors")}
|
||||
</label>
|
||||
</div>
|
||||
<div class="add-link-title-radios form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="link-type" value="hyper-link">
|
||||
${t("add_link.link_title_arbitrary")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="add-link-title-form-group form-group">
|
||||
<br/>
|
||||
<label>
|
||||
${t("add_link.link_title")}
|
||||
|
||||
<input class="link-title form-control" style="width: 100%;">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">${t("add_link.button_add_link")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class AddLinkDialog extends BasicWidget {
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $autoComplete!: JQuery<HTMLElement>;
|
||||
private $linkTitle!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleSettings!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleRadios!: JQuery<HTMLElement>;
|
||||
private $addLinkTitleFormGroup!: JQuery<HTMLElement>;
|
||||
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");
|
||||
}
|
||||
}
|
147
apps/client/src/widgets/dialogs/add_link.tsx
Normal file
147
apps/client/src/widgets/dialogs/add_link.tsx
Normal file
@ -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<LinkType>(hasSelection ? "hyper-link" : "reference-link");
|
||||
const [ suggestion, setSuggestion ] = useState<Suggestion>(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 (
|
||||
<Modal
|
||||
className="add-link-dialog"
|
||||
size="lg"
|
||||
maxWidth={1000}
|
||||
title={t("add_link.add_link")}
|
||||
helpPageId="QEAPj01N5f7w"
|
||||
footer={<Button text={t("add_link.button_add_link")} keyboardShortcut="Enter" />}
|
||||
onSubmit={onSubmit}
|
||||
onHidden={() => setSuggestion(null)}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label htmlFor="add-link-note-autocomplete">{t("add_link.note")}</label>
|
||||
|
||||
<NoteAutocomplete
|
||||
text={text}
|
||||
allowExternalLinks
|
||||
allowCreatingNotes
|
||||
onChange={setSuggestion}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!hasSelection && (
|
||||
<div className="add-link-title-settings">
|
||||
{(linkType !== "external-link") && (
|
||||
<>
|
||||
<FormRadioGroup
|
||||
name="link-type"
|
||||
currentValue={linkType}
|
||||
values={[
|
||||
{ value: "reference-link", label: t("add_link.link_title_mirrors") },
|
||||
{ value: "hyper-link", label: t("add_link.link_title_arbitrary") }
|
||||
]}
|
||||
onChange={(newValue) => setLinkType(newValue as LinkType)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(linkType !== "reference-link" && (
|
||||
<div className="add-link-title-form-group form-group">
|
||||
<br/>
|
||||
<label>
|
||||
{t("add_link.link_title")}
|
||||
|
||||
<input className="link-title form-control" style={{ width: "100%" }}
|
||||
value={linkTitle}
|
||||
onInput={(e: any) => setLinkTitle(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default class AddLinkDialog extends ReactBasicWidget {
|
||||
|
||||
private props: AddLinkDialogProps = {};
|
||||
|
||||
get component() {
|
||||
return <AddLinkDialogComponent {...this.props} />;
|
||||
}
|
||||
|
||||
async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
|
||||
this.props.text = text;
|
||||
this.props.textTypeWidget = textTypeWidget;
|
||||
this.doRender();
|
||||
await openDialog(this.$widget);
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ import { useRef } from "preact/hooks";
|
||||
|
||||
interface ButtonProps {
|
||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||
buttonRef: RefObject<HTMLButtonElement>;
|
||||
buttonRef?: RefObject<HTMLButtonElement>;
|
||||
text: string;
|
||||
className?: string;
|
||||
keyboardShortcut?: string;
|
||||
|
@ -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
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{title}</h5>
|
||||
{helpPageId && (
|
||||
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("branch_prefix.help_on_tree_prefix")}>?</button>
|
||||
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
||||
)}
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
||||
</div>
|
||||
|
48
apps/client/src/widgets/react/NoteAutocomplete.tsx
Normal file
48
apps/client/src/widgets/react/NoteAutocomplete.tsx
Normal file
@ -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<HTMLInputElement>(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 (
|
||||
<div className="input-group">
|
||||
<input
|
||||
ref={ref}
|
||||
className="note-autocomplete form-control"
|
||||
placeholder={t("add_link.search_note")} />
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user