refactor: add back UI for note_autocomplete

This commit is contained in:
JYC333 2026-03-09 13:28:27 +00:00
parent c0dd59458b
commit 8f6b565673
2 changed files with 245 additions and 16 deletions

View File

@ -65,15 +65,29 @@ interface ManagedInstance {
const instanceMap = new WeakMap<HTMLElement, ManagedInstance>();
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function normalizeAttributeSnippet(snippet: string): string {
return snippet.replace(/<br\s*\/?>/gi, " <span class=\"aa-core-separator\">&middot;</span> ");
}
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 ? `<div class="command-description">${escapeHtml(item.commandDescription)}</div>` : "";
const shortcutHtml = item.commandShortcut ? `<kbd class="command-shortcut">${escapeHtml(item.commandShortcut)}</kbd>` : "";
return `
<div class="command-suggestion">
<span class="command-icon ${iconClass}"></span>
<div class="command-content">
<div class="command-name">${titleHtml}</div>
${descriptionHtml}
</div>
${shortcutHtml}
</div>
`;
}
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"
? `<kbd class="aa-core-shortcut">Ctrl+Enter</kbd>`
: "";
const attributeHtml = item.highlightedAttributeSnippet
? `<div class="search-result-attributes">${normalizeAttributeSnippet(item.highlightedAttributeSnippet)}</div>`
: "";
const contentClass = item.action === "search-notes" ? "note-suggestion search-notes-action" : "note-suggestion";
return `
<div class="${contentClass}">
<span class="icon ${iconClass}"></span>
<span class="text">
<span class="aa-core-primary-row">
<span class="search-result-title">${titleHtml}</span>
${shortcutHtml}
</span>
${attributeHtml}
</span>
</div>
`;
}
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 })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
highlightedNotePathTitle: t("note_autocomplete.search-for", { term })
}
]);
}
@ -289,9 +397,14 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, 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);

View File

@ -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;