diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Web Clipper.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Web Clipper.html index 41eb7b54af..673ce63864 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Web Clipper.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Web Clipper.html @@ -30,43 +30,56 @@

The popup

Clicking the extension's button in the browser toolbar opens its popup:

Location of clippings

Trilium will save these clippings as a new child note under a "clipper diff --git a/apps/web-clipper/entrypoints/popup/main.tsx b/apps/web-clipper/entrypoints/popup/main.tsx index 1448a29845..a74980cdde 100644 --- a/apps/web-clipper/entrypoints/popup/main.tsx +++ b/apps/web-clipper/entrypoints/popup/main.tsx @@ -2,15 +2,13 @@ import "@/assets/theme.css"; import "./popup.css"; import type { IconData, IconDefinition } from "@boxicons/js"; -import ArrowLeft from "@boxicons/js/icons/ArrowLeft"; import Article from "@boxicons/js/icons/Article"; +import Bookmark from "@boxicons/js/icons/Bookmark"; import Cog from "@boxicons/js/icons/Cog"; import Crop from "@boxicons/js/icons/Crop"; -import FileX from "@boxicons/js/icons/FileX"; import Globe from "@boxicons/js/icons/Globe"; import HelpCircle from "@boxicons/js/icons/HelpCircle"; import Highlight from "@boxicons/js/icons/Highlight"; -import Link from "@boxicons/js/icons/Link"; import Lock from "@boxicons/js/icons/Lock"; import RefreshCw from "@boxicons/js/icons/RefreshCw"; import Screenshot from "@boxicons/js/icons/Screenshot"; @@ -59,8 +57,17 @@ interface TabContent { page: PageState; /** `null` when nothing is selected, or the page is out of reach. */ selection: SelectedContent | null; + /** The tab's own title and address, which a note clip saves. */ + title?: string; + url?: string; } +/** + * What the card saves: the selection, the readable page (`clipType: "page"`), or a note about the + * page with a link to it (`clipType: "note"`), which the popup calls a bookmark. + */ +type ClipMode = "selection" | "page" | "note"; + const PREVIEW_STYLE = ` :root { color-scheme: light dark; } body { margin: 8px; font: 12px/1.5 system-ui, sans-serif; overflow-wrap: anywhere; } @@ -70,6 +77,15 @@ const PREVIEW_STYLE = ` a { color: light-dark(#0076af, #95c3d9); text-decoration: none; pointer-events: none; } `; +const MODE_LABELS: Record = { selection: "Selection", page: "Page", note: "Bookmark" }; + +/** The save button of each mode, with the command whose shortcut it shows, or its own shortcut. */ +const SAVE_BUTTONS: Record = { + selection: { icon: Highlight, label: "Save selection", command: "saveSelection" }, + page: { icon: Article, label: "Save page to Trilium", command: "saveWholePage" }, + note: { icon: Bookmark, label: "Save bookmark", command: "", shortcut: "Ctrl+Enter" } +}; + const root = document.getElementById("root"); if (root) { render(, root); @@ -78,7 +94,6 @@ if (root) { export function Popup() { const [ searchStatus, setSearchStatus ] = useState(); const [ clippedNoteId, setClippedNoteId ] = useState(null); - const [ isWritingNote, setIsWritingNote ] = useState(false); const [ shortcuts, setShortcuts ] = useState({}); useEffect(() => { @@ -106,17 +121,11 @@ export function Popup() { const isMismatch = searchStatus?.status === "version-mismatch"; let body: ComponentChildren; - if (isWritingNote) { - body = setIsWritingNote(false)} />; - } else if (searchStatus?.status === "not-found") { + if (searchStatus?.status === "not-found") { body = ; } else { body = ( - setIsWritingNote(true)} - /> + ); } @@ -174,11 +183,7 @@ export function Popup() { ); } -function CaptureActions({ disabled, shortcuts, onWriteNote }: { - disabled: boolean; - shortcuts: Shortcuts; - onWriteNote: () => void; -}) { +function CaptureActions({ disabled, shortcuts }: { disabled: boolean, shortcuts: Shortcuts }) { /** `undefined` while the page is read. */ const [ tab, setTab ] = useState(); @@ -201,17 +206,8 @@ function CaptureActions({ disabled, shortcuts, onWriteNote }: { {"The extension cannot access this page.\nIf it is a regular web page, reload it."} ); - } else if (tab?.page === "unreadable" && !tab.selection) { - main = This page has no article to save.; } else { - main = ( - - ); + main = ; } return ( @@ -229,10 +225,6 @@ function CaptureActions({ disabled, shortcuts, onWriteNote }: { disabledReason={pageDisabledReason} onClick={() => sendAndClose("save-whole-screenshot")} /> - ("selection"); + const [ chosenMode, setChosenMode ] = useState(); /** `undefined` until the user edits the title. */ const [ title, setTitle ] = useState(); + const [ text, setText ] = useState(""); + const textAreaRef = useRef(null); - const clip = selection && (mode === "selection" || !article) ? selection : article; - const isSelection = !!clip && clip === selection; - const shownTitle = title ?? (article ?? selection)?.title ?? ""; + const article = typeof tab?.page === "object" ? tab.page : undefined; + const selection = tab?.selection ?? undefined; + const modes = tab ? availableModes(article, selection) : []; + const mode = chosenMode && modes.includes(chosenMode) ? chosenMode : modes[0]; + const clip = mode === "selection" ? selection : mode === "page" ? article : undefined; + const shownTitle = title ?? (article ?? selection)?.title ?? tab?.title ?? ""; - function save(content: SelectedContent | ExtractedPage) { + useEffect(() => { + if (mode === "note") textAreaRef.current?.focus(); + }, [ mode ]); + + /** Keeps the popup open when the note could not be saved, so its text is not lost. */ + async function saveNote() { + const content = text.trim(); + const result = await sendMessage({ + name: "save-link-with-note", + title: shownTitle.trim(), + content: content ? textToHtml(content) : "" + }); + if (result) window.close(); + } + + function saveClip(content: SelectedContent) { const titled = { ...content, title: shownTitle.trim() || content.title }; - void sendMessage(isSelection + void sendMessage(mode === "selection" ? { name: "save-selection", selection: titled } : { name: "save-whole-page", page: titled }); window.close(); } - const published = clip && !isSelection ? article?.labels.publishedDate : undefined; + function saveNoteOnCtrlEnter(e: KeyboardEvent) { + if (mode === "note" && e.key === "Enter" && e.ctrlKey) { + e.preventDefault(); + void saveNote(); + } + } + + let onSave: (() => void) | undefined; + if (mode === "note") { + onSave = () => void saveNote(); + } else if (clip) { + onSave = () => saveClip(clip); + } + + const pageUrl = mode === "note" ? tab?.url : clip?.pageUrl; + const published = mode === "page" ? article?.labels.publishedDate : undefined; + const button = SAVE_BUTTONS[mode ?? "page"]; return (

- {clip && ( + {mode && (
@@ -321,41 +348,69 @@ function PagePreview({ article, selection, disabled, shortcuts }: { placeholder="Note title" value={shownTitle} onInput={(e) => setTitle(e.currentTarget.value)} + onKeyDown={saveNoteOnCtrlEnter} /> -
- {new URL(clip.pageUrl).hostname}{published && ` · Published ${published}`} -
+ {pageUrl && ( +
+ {new URL(pageUrl).hostname}{published && ` · Published ${published}`} +
+ )}
)} - {article && selection && ( + {modes.length > 1 && (
- - + {modes.map((option) => ( + + ))}
)} + {mode === "note" && modes.length === 1 && ( +
This page has no article; save it as a bookmark.
+ )} +
- {clip - ?