Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions apps/client/src/services/note_autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface Suggestion {
notePathTitle?: string;
notePath?: string;
highlightedNotePathTitle?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
action?: string | "create-note" | "search-notes" | "external-link" | "command";
parentNoteId?: string;
icon?: string;
Expand Down Expand Up @@ -308,11 +310,12 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
displayKey: "notePathTitle",
templates: {
suggestion: (suggestion) => {
// Handle different suggestion types
if (suggestion.action === "command") {
let html = `<div class="command-suggestion">`;
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
html += `<div class="command-content">`;
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
html += `<div class="command-name">${suggestion.highlightedNotePathTitle || suggestion.noteTitle || ''}</div>`;
if (suggestion.commandDescription) {
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
}
Expand All @@ -323,7 +326,20 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
html += '</div>';
return html;
}
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;

// For note suggestions, match Quick Search structure
// Title row with icon
let html = `<div style="display: flex; align-items: center; gap: 6px;">`;
html += `<span class="${suggestion.icon ?? "bx bx-note"}" style="flex-shrink: 0;"></span>`;
html += `<span class="search-result-title" style="flex: 1;">${suggestion.highlightedNotePathTitle || ''}</span>`;
html += `</div>`;

// Add attribute snippet if available (inline display)
if (suggestion.highlightedAttributeSnippet && suggestion.highlightedAttributeSnippet.trim()) {
html += `<div class="search-result-attributes" style="margin-left: 20px; margin-top: 2px; color: var(--muted-text-color); font-size: 0.9em;">${suggestion.highlightedAttributeSnippet}</div>`;
}

return html;
}
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
Expand Down
62 changes: 62 additions & 0 deletions apps/client/src/services/quick_search_renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Quick Search specific result renderer
*
* This module provides HTML rendering functionality specifically for the Quick Search widget.
* The Jump To dialog (note_autocomplete) intentionally has its own inline rendering logic
* with different styling and layout requirements.
*
* SECURITY NOTE: HTML Snippet Handling
* The highlighted snippet fields (highlightedAttributeSnippet) contain
* pre-sanitized HTML from the server. The server-side processing:
* 1. Escapes all HTML using the escape-html library
* 2. Adds safe HTML tags for display: <b> for search term highlighting
* 3. See apps/server/src/services/search/services/search.ts for implementation
*
* This means the HTML snippets can be safely inserted without additional escaping on the client side.
*/

import type { Suggestion } from "./note_autocomplete.js";

/**
* Creates HTML for a Quick Search result item
*
* @param result - The search result item to render
* @returns HTML string formatted for Quick Search widget display
*/
export function createSearchResultHtml(result: Suggestion): string {
// Handle command action
if (result.action === "command") {
let html = `<div class="command-suggestion">`;
html += `<span class="command-icon ${result.icon || "bx bx-terminal"}"></span>`;
html += `<div class="command-content">`;
html += `<div class="command-name">${result.highlightedNotePathTitle || ''}</div>`;
if (result.commandDescription) {
html += `<div class="command-description">${result.commandDescription}</div>`;
}
html += `</div>`;
if (result.commandShortcut) {
html += `<kbd class="command-shortcut">${result.commandShortcut}</kbd>`;
}
html += '</div>';
return html;
}

// Default: render as note result
// Wrap everything in a flex column container
let itemHtml = `<div style="display: flex; flex-direction: column; gap: 2px;">`;

// Title row with icon
itemHtml += `<div style="display: flex; align-items: center; gap: 6px;">`;
itemHtml += `<span class="${result.icon || 'bx bx-note'}" style="flex-shrink: 0;"></span>`;
itemHtml += `<span class="search-result-title" style="flex: 1;">${result.highlightedNotePathTitle || result.notePathTitle || ''}</span>`;
itemHtml += `</div>`;

// Add attribute snippet if available (inline display)
if (result.highlightedAttributeSnippet && result.highlightedAttributeSnippet.trim()) {
itemHtml += `<div class="search-result-attributes" style="margin-left: 20px; color: var(--muted-text-color); font-size: 0.9em;">${result.highlightedAttributeSnippet}</div>`;
}

itemHtml += `</div>`;

return itemHtml;
}
55 changes: 51 additions & 4 deletions apps/client/src/stylesheets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,25 @@ table.promoted-attributes-in-tooltip th {

.aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 5px;
padding: 12px 16px;
margin: 0;
line-height: 1.4;
position: relative;
white-space: normal;
}

/* Add separator between Jump To suggestions like Quick Search */
.jump-to-note-results .aa-suggestion:not(:last-child)::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 2px;
background: var(--main-border-color);
border-radius: 1px;
opacity: 0.4;
}

.aa-dropdown-menu .aa-suggestion p {
Expand Down Expand Up @@ -1786,7 +1803,7 @@ textarea {
}

.jump-to-note-results .aa-suggestions {
padding: 1rem;
padding: 0;
}

/* Command palette styling */
Expand Down Expand Up @@ -2260,13 +2277,43 @@ footer.webview-footer button {
padding: 1px 10px 1px 10px;
}

/* Search result highlighting */
/* Search result highlighting - applies to both Quick Search and Jump To */
.search-result-title b,
.search-result-content b {
.search-result-content b,
.search-result-attributes b,
.quick-search .search-result-title b,
.quick-search .search-result-content b,
.quick-search .search-result-attributes b {
font-weight: 900;
color: var(--admonition-warning-accent-color);
}

/* Quick Search specific snippet styling */
.quick-search .search-result-content {
font-size: 0.85em;
color: var(--main-text-color);
opacity: 0.7;
}

.quick-search .search-result-attributes {
font-size: 0.75em;
color: var(--muted-text-color);
opacity: 0.5;
}

/* Jump To (autocomplete) specific snippet styling */
.aa-dropdown-menu .search-result-content {
font-size: 0.82em;
color: var(--main-text-color);
opacity: 0.6;
}

.aa-dropdown-menu .search-result-attributes {
font-size: 0.75em;
color: var(--muted-text-color);
opacity: 0.5;
}

/* Customized icons */

.bx-tn-toc::before {
Expand Down
9 changes: 4 additions & 5 deletions apps/client/src/stylesheets/theme-next/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -530,17 +530,16 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
}

/* List item */
.jump-to-note-dialog .aa-suggestions div,
.note-detail-empty .aa-suggestions div {
.jump-to-note-dialog .aa-suggestions .aa-suggestion,
.note-detail-empty .aa-suggestions .aa-suggestion {
border-radius: 6px;
padding: 6px 12px;
color: var(--menu-text-color);
cursor: default;
}

/* Selected list item */
.jump-to-note-dialog .aa-suggestions div.aa-cursor,
.note-detail-empty .aa-suggestions div.aa-cursor {
.jump-to-note-dialog .aa-suggestions .aa-suggestion.aa-cursor,
.note-detail-empty .aa-suggestions .aa-suggestion.aa-cursor {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
29 changes: 9 additions & 20 deletions apps/client/src/widgets/quick_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import appContext from "../components/app_context.js";
import shortcutService from "../services/shortcuts.js";
import { t } from "../services/i18n.js";
import { Dropdown, Tooltip } from "bootstrap";
import { createSearchResultHtml } from "../services/quick_search_renderer.js";

const TPL = /*html*/`
<div class="quick-search input-group input-group-sm">
Expand Down Expand Up @@ -91,8 +92,6 @@ interface QuickSearchResponse {
noteTitle: string;
notePathTitle: string;
highlightedNotePathTitle: string;
contentSnippet?: string;
highlightedContentSnippet?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
icon: string;
Expand Down Expand Up @@ -236,24 +235,14 @@ export default class QuickSearchWidget extends BasicWidget {

const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');

// Build the display HTML with content snippet below the title
let itemHtml = `<div style="display: flex; flex-direction: column;">
<div style="display: flex; align-items: flex-start; gap: 6px;">
<span class="${result.icon}" style="flex-shrink: 0; margin-top: 1px;"></span>
<span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span>
</div>`;

// Add attribute snippet (tags/attributes) below the title if available
if (result.highlightedAttributeSnippet) {
itemHtml += `<div style="font-size: 0.75em; color: var(--muted-text-color); opacity: 0.5; margin-left: 20px; margin-top: 2px; line-height: 1.2;" class="search-result-attributes">${result.highlightedAttributeSnippet}</div>`;
}

// Add content snippet below the attributes if available
if (result.highlightedContentSnippet) {
itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`;
}

itemHtml += `</div>`;
// Use the shared renderer for consistent display
const itemHtml = createSearchResultHtml({
icon: result.icon,
notePathTitle: result.notePathTitle,
highlightedNotePathTitle: result.highlightedNotePathTitle,
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
highlightedContentSnippet: result.highlightedContentSnippet
});

$item.html(itemHtml);

Expand Down
2 changes: 0 additions & 2 deletions apps/server/src/services/search/search_result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ class SearchResult {
score: number;
notePathTitle: string;
highlightedNotePathTitle?: string;
contentSnippet?: string;
highlightedContentSnippet?: string;
attributeSnippet?: string;
highlightedAttributeSnippet?: string;
private fuzzyScore: number; // Track fuzzy score separately
Expand Down
Loading
Loading