mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(client): try to stylize the quick search even further in the client
This commit is contained in:
parent
6c79be881d
commit
2d358342c5
@ -23,12 +23,21 @@ const TPL = /*html*/`
|
||||
|
||||
.quick-search .dropdown-menu {
|
||||
max-height: 600px;
|
||||
max-width: 400px;
|
||||
max-width: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: -30px 50px 93px -50px black;
|
||||
}
|
||||
|
||||
.quick-search .dropdown-item {
|
||||
white-space: normal;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.quick-search .dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="input-group-prepend">
|
||||
@ -50,6 +59,8 @@ interface QuickSearchResponse {
|
||||
noteTitle: string;
|
||||
notePathTitle: string;
|
||||
highlightedNotePathTitle: string;
|
||||
contentSnippet?: string;
|
||||
highlightedContentSnippet?: string;
|
||||
icon: string;
|
||||
}>;
|
||||
error: string;
|
||||
@ -151,7 +162,16 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
if (!noteId) continue;
|
||||
|
||||
const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');
|
||||
$item.html(`<span class="${result.icon}"></span> ${result.highlightedNotePathTitle}`);
|
||||
|
||||
// Build the display HTML
|
||||
let itemHtml = `<div><span class="${result.icon}"></span> ${result.highlightedNotePathTitle}</div>`;
|
||||
|
||||
// Add content snippet if available
|
||||
if (result.highlightedContentSnippet) {
|
||||
itemHtml += `<div style="font-size: 0.85em; color: #666; margin-left: 20px; margin-top: 2px;">${result.highlightedContentSnippet}</div>`;
|
||||
}
|
||||
|
||||
$item.html(itemHtml);
|
||||
|
||||
$item.on("click", (e) => {
|
||||
this.dropdown.hide();
|
||||
|
@ -29,6 +29,8 @@ class SearchResult {
|
||||
score: number;
|
||||
notePathTitle: string;
|
||||
highlightedNotePathTitle?: string;
|
||||
contentSnippet?: string;
|
||||
highlightedContentSnippet?: string;
|
||||
|
||||
constructor(notePathArray: string[]) {
|
||||
this.notePathArray = notePathArray;
|
||||
|
@ -17,6 +17,8 @@ import type { SearchParams, TokenStructure } from "./types.js";
|
||||
import type Expression from "../expressions/expression.js";
|
||||
import sql from "../../sql.js";
|
||||
import scriptService from "../../script.js";
|
||||
import striptags from "striptags";
|
||||
import protectedSessionService from "../../protected_session.js";
|
||||
|
||||
export interface SearchNoteResult {
|
||||
searchResultNoteIds: string[];
|
||||
@ -337,6 +339,91 @@ function findFirstNoteWithQuery(query: string, searchContext: SearchContext): BN
|
||||
return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null;
|
||||
}
|
||||
|
||||
function extractContentSnippet(noteId: string, searchTokens: string[], maxLength: number = 200): string {
|
||||
const note = becca.notes[noteId];
|
||||
if (!note) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Only extract content for text-based notes
|
||||
if (!["text", "code", "mermaid", "canvas", "mindMap"].includes(note.type)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
let content = note.getContent();
|
||||
|
||||
if (!content || typeof content !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Handle protected notes
|
||||
if (note.isProtected && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
try {
|
||||
content = protectedSessionService.decryptString(content) || "";
|
||||
} catch (e) {
|
||||
return ""; // Can't decrypt, don't show content
|
||||
}
|
||||
} else if (note.isProtected) {
|
||||
return ""; // Protected but no session available
|
||||
}
|
||||
|
||||
// Strip HTML tags for text notes
|
||||
if (note.type === "text") {
|
||||
content = striptags(content);
|
||||
}
|
||||
|
||||
// Normalize whitespace
|
||||
content = content.replace(/\s+/g, " ").trim();
|
||||
|
||||
if (!content) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try to find a snippet around the first matching token
|
||||
const normalizedContent = normalizeString(content.toLowerCase());
|
||||
let snippetStart = 0;
|
||||
let matchFound = false;
|
||||
|
||||
for (const token of searchTokens) {
|
||||
const normalizedToken = normalizeString(token.toLowerCase());
|
||||
const matchIndex = normalizedContent.indexOf(normalizedToken);
|
||||
|
||||
if (matchIndex !== -1) {
|
||||
// Center the snippet around the match
|
||||
snippetStart = Math.max(0, matchIndex - maxLength / 2);
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract snippet
|
||||
let snippet = content.substring(snippetStart, snippetStart + maxLength);
|
||||
|
||||
// Try to start/end at word boundaries
|
||||
if (snippetStart > 0) {
|
||||
const firstSpace = snippet.indexOf(" ");
|
||||
if (firstSpace > 0 && firstSpace < 20) {
|
||||
snippet = snippet.substring(firstSpace + 1);
|
||||
}
|
||||
snippet = "..." + snippet;
|
||||
}
|
||||
|
||||
if (snippetStart + maxLength < content.length) {
|
||||
const lastSpace = snippet.lastIndexOf(" ");
|
||||
if (lastSpace > snippet.length - 20) {
|
||||
snippet = snippet.substring(0, lastSpace);
|
||||
}
|
||||
snippet = snippet + "...";
|
||||
}
|
||||
|
||||
return snippet;
|
||||
} catch (e) {
|
||||
log.error(`Error extracting content snippet for note ${noteId}: ${e}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: fastSearch,
|
||||
@ -351,6 +438,11 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
||||
|
||||
const trimmed = allSearchResults.slice(0, 200);
|
||||
|
||||
// Extract content snippets
|
||||
for (const result of trimmed) {
|
||||
result.contentSnippet = extractContentSnippet(result.noteId, searchContext.highlightedTokens);
|
||||
}
|
||||
|
||||
highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
|
||||
|
||||
return trimmed.map((result) => {
|
||||
@ -360,6 +452,8 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
||||
noteTitle: title,
|
||||
notePathTitle: result.notePathTitle,
|
||||
highlightedNotePathTitle: result.highlightedNotePathTitle,
|
||||
contentSnippet: result.contentSnippet,
|
||||
highlightedContentSnippet: result.highlightedContentSnippet,
|
||||
icon: icon ?? "bx bx-note"
|
||||
};
|
||||
});
|
||||
@ -381,26 +475,11 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
highlightedTokens.sort((a, b) => (a.length > b.length ? -1 : 1));
|
||||
|
||||
for (const result of searchResults) {
|
||||
const note = becca.notes[result.noteId];
|
||||
|
||||
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
|
||||
|
||||
if (highlightedTokens.find((token) => note.type.includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "type: ${note.type}'`;
|
||||
}
|
||||
|
||||
if (highlightedTokens.find((token) => note.mime.includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "mime: ${note.mime}'`;
|
||||
}
|
||||
|
||||
for (const attr of note.getAttributes()) {
|
||||
if (attr.type === "relation" && attr.name === "internalLink" && ignoreInternalAttributes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (highlightedTokens.find((token) => normalize(attr.name).includes(token) || normalize(attr.value).includes(token))) {
|
||||
result.highlightedNotePathTitle += ` "${formatAttribute(attr)}'`;
|
||||
}
|
||||
// Initialize highlighted content snippet
|
||||
if (result.contentSnippet) {
|
||||
result.highlightedContentSnippet = escapeHtml(result.contentSnippet).replace(/[<{}]/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
@ -419,40 +498,36 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
||||
const tokenRegex = new RegExp(escapeRegExp(token), "gi");
|
||||
let match;
|
||||
|
||||
// Find all matches
|
||||
if (!result.highlightedNotePathTitle) {
|
||||
continue;
|
||||
// Highlight in note path title
|
||||
if (result.highlightedNotePathTitle) {
|
||||
const titleRegex = new RegExp(escapeRegExp(token), "gi");
|
||||
while ((match = titleRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
|
||||
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
|
||||
// 2 characters are added, so we need to adjust the index
|
||||
titleRegex.lastIndex += 2;
|
||||
}
|
||||
}
|
||||
while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) {
|
||||
result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}");
|
||||
|
||||
// 2 characters are added, so we need to adjust the index
|
||||
tokenRegex.lastIndex += 2;
|
||||
// Highlight in content snippet
|
||||
if (result.highlightedContentSnippet) {
|
||||
const contentRegex = new RegExp(escapeRegExp(token), "gi");
|
||||
while ((match = contentRegex.exec(normalizeString(result.highlightedContentSnippet))) !== null) {
|
||||
result.highlightedContentSnippet = wrapText(result.highlightedContentSnippet, match.index, token.length, "{", "}");
|
||||
// 2 characters are added, so we need to adjust the index
|
||||
contentRegex.lastIndex += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const result of searchResults) {
|
||||
if (!result.highlightedNotePathTitle) {
|
||||
continue;
|
||||
}
|
||||
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/"/g, "<small>").replace(/'/g, "</small>").replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||
}
|
||||
}
|
||||
|
||||
function formatAttribute(attr: BAttribute) {
|
||||
if (attr.type === "relation") {
|
||||
return `~${escapeHtml(attr.name)}=…`;
|
||||
} else if (attr.type === "label") {
|
||||
let label = `#${escapeHtml(attr.name)}`;
|
||||
|
||||
if (attr.value) {
|
||||
const val = /[^\w-]/.test(attr.value) ? `"${attr.value}"` : attr.value;
|
||||
|
||||
label += `=${escapeHtml(val)}`;
|
||||
if (result.highlightedNotePathTitle) {
|
||||
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||
}
|
||||
|
||||
return label;
|
||||
if (result.highlightedContentSnippet) {
|
||||
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user