From 8f6b565673d1ab903266e9936c2fdb3591134cff Mon Sep 17 00:00:00 2001 From: JYC333 <22962980+JYC333@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:28:27 +0000 Subject: [PATCH] refactor: add back UI for note_autocomplete --- apps/client/src/services/note_autocomplete.ts | 143 ++++++++++++++++-- apps/client/src/stylesheets/style.css | 118 ++++++++++++++- 2 files changed, 245 insertions(+), 16 deletions(-) diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index 9822793a57..0e8f530d8d 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -65,15 +65,29 @@ interface ManagedInstance { const instanceMap = new WeakMap(); -function createPanelEl(): HTMLElement { +function createPanelEl(container?: HTMLElement | null): HTMLElement { const panel = document.createElement("div"); - panel.className = "aa-core-panel"; + panel.className = "aa-core-panel aa-dropdown-menu"; + if (container) { + panel.classList.add("aa-core-panel--contained"); + container.appendChild(panel); + } else { + document.body.appendChild(panel); + } panel.style.display = "none"; - document.body.appendChild(panel); return panel; } function positionPanel(panelEl: HTMLElement, inputEl: HTMLElement): void { + if (panelEl.classList.contains("aa-core-panel--contained")) { + panelEl.style.position = "static"; + panelEl.style.top = ""; + panelEl.style.left = ""; + panelEl.style.width = "100%"; + panelEl.style.display = "block"; + return; + } + const rect = inputEl.getBoundingClientRect(); panelEl.style.position = "fixed"; panelEl.style.top = `${rect.bottom}px`; @@ -82,25 +96,119 @@ function positionPanel(panelEl: HTMLElement, inputEl: HTMLElement): void { panelEl.style.display = "block"; } +function escapeHtml(text: string): string { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function normalizeAttributeSnippet(snippet: string): string { + return snippet.replace(//gi, " · "); +} + +function getSuggestionIconClass(item: Suggestion): string { + if (item.action === "search-notes") { + return "bx bx-search"; + } + if (item.action === "create-note") { + return "bx bx-plus"; + } + if (item.action === "external-link") { + return "bx bx-link-external"; + } + + return item.icon || "bx bx-note"; +} + +function renderCommandSuggestion(item: Suggestion): string { + const iconClass = escapeHtml(item.icon || "bx bx-terminal"); + const titleHtml = item.highlightedNotePathTitle || escapeHtml(item.noteTitle || ""); + const descriptionHtml = item.commandDescription ? `
${escapeHtml(item.commandDescription)}
` : ""; + const shortcutHtml = item.commandShortcut ? `${escapeHtml(item.commandShortcut)}` : ""; + + return ` +
+ +
+
${titleHtml}
+ ${descriptionHtml} +
+ ${shortcutHtml} +
+ `; +} + +function renderNoteSuggestion(item: Suggestion): string { + const iconClass = escapeHtml(getSuggestionIconClass(item)); + const titleHtml = item.highlightedNotePathTitle || escapeHtml(item.noteTitle || item.notePathTitle || item.externalLink || ""); + const shortcutHtml = item.action === "search-notes" + ? `Ctrl+Enter` + : ""; + const attributeHtml = item.highlightedAttributeSnippet + ? `
${normalizeAttributeSnippet(item.highlightedAttributeSnippet)}
` + : ""; + const contentClass = item.action === "search-notes" ? "note-suggestion search-notes-action" : "note-suggestion"; + + return ` +
+ + + + ${titleHtml} + ${shortcutHtml} + + ${attributeHtml} + +
+ `; +} + +function renderSuggestion(item: Suggestion): string { + if (item.action === "command") { + return renderCommandSuggestion(item); + } + + return renderNoteSuggestion(item); +} + function renderItems(panelEl: HTMLElement, items: Suggestion[], activeId: number | null, onSelect: (item: Suggestion) => void) { if (items.length === 0) { panelEl.style.display = "none"; return; } - const list = document.createElement("ul"); - list.className = "aa-core-list"; + + const list = document.createElement("div"); + list.className = "aa-core-list aa-suggestions"; + list.setAttribute("role", "listbox"); + items.forEach((item, index) => { - const li = document.createElement("li"); - li.className = "aa-core-item"; - if (index === activeId) li.classList.add("aa-core-item--active"); - - // Very basic simple UI for step 3.1 - li.textContent = item.highlightedNotePathTitle || item.noteTitle || ""; - li.onmousedown = (e) => { e.preventDefault(); onSelect(item); }; - list.appendChild(li); + const itemEl = document.createElement("div"); + itemEl.className = "aa-core-item aa-suggestion"; + itemEl.setAttribute("role", "option"); + itemEl.setAttribute("aria-selected", index === activeId ? "true" : "false"); + + if (item.action) { + itemEl.classList.add(`${item.action}-action`); + } + if (index === activeId) { + itemEl.classList.add("aa-core-item--active", "aa-cursor"); + } + + itemEl.innerHTML = renderSuggestion(item); + itemEl.onmousedown = (e) => { + e.preventDefault(); + onSelect(item); + }; + + list.appendChild(itemEl); }); + panelEl.innerHTML = ""; panelEl.appendChild(list); + panelEl.style.display = "block"; } async function autocompleteSourceForCKEditor(queryText: string) { @@ -191,7 +299,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void { action: "search-notes", noteTitle: term, - highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} Ctrl+Enter` + highlightedNotePathTitle: t("note_autocomplete.search-for", { term }) } ]); } @@ -289,9 +397,14 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { options = options || {}; - const panelEl = createPanelEl(); + const panelEl = createPanelEl(options.container); let rafId: number | null = null; function startPositioning() { + if (panelEl.classList.contains("aa-core-panel--contained")) { + positionPanel(panelEl, inputEl); + return; + } + if (rafId !== null) return; const update = () => { positionPanel(panelEl, inputEl); diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index f7e901ca5a..f40ca46933 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -974,6 +974,17 @@ table.promoted-attributes-in-tooltip th { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } +.aa-core-panel.aa-dropdown-menu { + width: 100%; +} + +.aa-core-panel--contained { + position: static !important; + border: 0; + background: transparent; + box-shadow: none; +} + .aa-core-list { list-style: none; padding: 0; @@ -982,8 +993,9 @@ table.promoted-attributes-in-tooltip th { .aa-core-item { cursor: pointer; - padding: 6px 16px; + padding: 7px 16px; margin: 0; + white-space: normal; } .aa-core-item:hover, @@ -992,6 +1004,110 @@ table.promoted-attributes-in-tooltip th { background-color: var(--active-item-background-color); } +.aa-core-item .note-suggestion { + display: flex; + align-items: flex-start; + gap: 8px; + width: 100%; +} + +.aa-core-item .icon, +.aa-core-item .command-icon { + flex-shrink: 0; + line-height: 1.4; + margin-top: 1px; +} + +.aa-core-item .text { + min-width: 0; + flex: 1; +} + +.aa-core-item .aa-core-primary-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +.aa-core-item .search-result-title { + display: block; + min-width: 0; + line-height: 1.35; + word-break: break-word; + font-size: 1.02em; +} + +.aa-core-item .search-result-attributes { + display: block; + margin-top: 1px; + font-size: 0.8em; + color: var(--muted-text-color); + opacity: 0.65; + line-height: 1.2; + word-break: break-word; +} + +.aa-core-item .search-result-attributes { + padding-inline-start: 14px; +} + +.aa-core-item .aa-core-shortcut, +.aa-core-item kbd.command-shortcut { + flex-shrink: 0; + padding: 0; + border: 0; + background: transparent; + color: var(--muted-text-color); + font-family: inherit !important; + font-size: 0.8em; + opacity: 0.85; +} + +.aa-core-item .command-suggestion { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + font-size: 0.9em; +} + +.aa-core-item .command-content { + flex-grow: 1; + min-width: 0; +} + +.aa-core-item .command-name { + font-weight: bold; + line-height: 1.35; +} + +.aa-core-item .command-description { + font-size: 0.8em; + line-height: 1.3; + opacity: 0.75; +} + +.aa-core-item .search-result-title b, +.aa-core-item .search-result-path b, +.aa-core-item .search-result-attributes b, +.aa-core-item .command-name b, +.aa-core-item .command-description b { + color: var(--admonition-warning-accent-color); + text-decoration: underline; +} + +.aa-core-item .aa-core-separator { + padding: 0 2px; +} + +.jump-to-note-results .aa-core-panel--contained { + max-height: calc(80vh - 200px); + overflow-y: auto; + overflow-x: hidden; + text-overflow: ellipsis; +} + .help-button { float: inline-end; background: none;