mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			feat/fix-j
			...
			feat/ui-op
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0a9c0234e2 | ||
|  | 3e3cc8c541 | ||
|  | d1538508e8 | ||
|  | 9b1da8c311 | ||
|  | e4a8258acf | ||
|  | 5e88043c7b | ||
|  | bedf9112fb | ||
|  | 03681d23c5 | 
| @@ -30,8 +30,6 @@ 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; | ||||
| @@ -310,12 +308,11 @@ 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 || suggestion.noteTitle || ''}</div>`; | ||||
|                             html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`; | ||||
|                             if (suggestion.commandDescription) { | ||||
|                                 html += `<div class="command-description">${suggestion.commandDescription}</div>`; | ||||
|                             } | ||||
| @@ -326,20 +323,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | ||||
|                             html += '</div>'; | ||||
|                             return html; | ||||
|                         } | ||||
|                          | ||||
|                         // 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; | ||||
|                         return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`; | ||||
|                     } | ||||
|                 }, | ||||
|                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added | ||||
|   | ||||
| @@ -1,62 +0,0 @@ | ||||
| /** | ||||
|  * 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; | ||||
| } | ||||
| @@ -28,6 +28,14 @@ | ||||
|     --ck-mention-list-max-height: 500px; | ||||
| } | ||||
|  | ||||
| body#trilium-app.motion-disabled *, | ||||
| body#trilium-app.motion-disabled *::before, | ||||
| body#trilium-app.motion-disabled *::after { | ||||
|     /* Disable transitions and animations */ | ||||
|     transition: none !important; | ||||
|     animation: none !important; | ||||
| } | ||||
|  | ||||
| .table { | ||||
|     --bs-table-bg: transparent !important; | ||||
| } | ||||
| @@ -355,7 +363,7 @@ body.desktop .tabulator-popup-container { | ||||
|  | ||||
| @supports (animation-fill-mode: forwards) { | ||||
|     /* Delay the opening of submenus */ | ||||
|     body.desktop .dropdown-submenu .dropdown-menu { | ||||
|     body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu { | ||||
|         opacity: 0; | ||||
|         animation-fill-mode: forwards; | ||||
|         animation-delay: var(--submenu-opening-delay); | ||||
| @@ -840,25 +848,8 @@ table.promoted-attributes-in-tooltip th { | ||||
|  | ||||
| .aa-dropdown-menu .aa-suggestion { | ||||
|     cursor: pointer; | ||||
|     padding: 12px 16px; | ||||
|     padding: 5px; | ||||
|     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 { | ||||
| @@ -1803,7 +1794,7 @@ textarea { | ||||
| } | ||||
|  | ||||
| .jump-to-note-results .aa-suggestions { | ||||
|     padding: 0; | ||||
|     padding: 1rem; | ||||
| } | ||||
|  | ||||
| /* Command palette styling */ | ||||
| @@ -2277,43 +2268,13 @@ footer.webview-footer button { | ||||
|     padding: 1px 10px 1px 10px; | ||||
| } | ||||
|  | ||||
| /* Search result highlighting - applies to both Quick Search and Jump To */ | ||||
| /* Search result highlighting */ | ||||
| .search-result-title 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 { | ||||
| .search-result-content 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 { | ||||
|   | ||||
| @@ -530,16 +530,17 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after { | ||||
| } | ||||
|  | ||||
| /* List item */ | ||||
| .jump-to-note-dialog .aa-suggestions .aa-suggestion, | ||||
| .note-detail-empty .aa-suggestions .aa-suggestion { | ||||
| .jump-to-note-dialog .aa-suggestions div, | ||||
| .note-detail-empty .aa-suggestions div { | ||||
|     border-radius: 6px; | ||||
|     padding: 6px 12px; | ||||
|     color: var(--menu-text-color); | ||||
|     cursor: default; | ||||
| } | ||||
|  | ||||
| /* Selected list item */ | ||||
| .jump-to-note-dialog .aa-suggestions .aa-suggestion.aa-cursor, | ||||
| .note-detail-empty .aa-suggestions .aa-suggestion.aa-cursor { | ||||
| .jump-to-note-dialog .aa-suggestions div.aa-cursor, | ||||
| .note-detail-empty .aa-suggestions div.aa-cursor { | ||||
|     background: var(--hover-item-background-color); | ||||
|     color: var(--hover-item-text-color); | ||||
| } | ||||
| @@ -1113,6 +1113,10 @@ | ||||
|     "layout-vertical-description": "launcher bar is on the left (default)", | ||||
|     "layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width." | ||||
|   }, | ||||
|   "ui-performance": { | ||||
|     "title": "Performance", | ||||
|     "enable-motion": "Enable transitions and animations" | ||||
|   }, | ||||
|   "ai_llm": { | ||||
|     "not_started": "Not started", | ||||
|     "title": "AI Settings", | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import utils from "../../services/utils.js"; | ||||
| import type BasicWidget from "../basic_widget.js"; | ||||
| import { EventData } from "../../components/app_context.js"; | ||||
| import FlexContainer from "./flex_container.js"; | ||||
| import options from "../../services/options.js"; | ||||
| import type BasicWidget from "../basic_widget.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
|  | ||||
| /** | ||||
|  * The root container is the top-most widget/container, from which the entire layout derives. | ||||
| @@ -20,6 +22,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> { | ||||
|         this.id("root-widget"); | ||||
|         this.css("height", "100dvh"); | ||||
|         this.originalViewportHeight = getViewportHeight(); | ||||
|          | ||||
|     } | ||||
|  | ||||
|     render(): JQuery<HTMLElement> { | ||||
| @@ -27,15 +30,27 @@ export default class RootContainer extends FlexContainer<BasicWidget> { | ||||
|             window.visualViewport?.addEventListener("resize", () => this.#onMobileResize()); | ||||
|         } | ||||
|  | ||||
|         this.#setMotion(options.is("motionEnabled")); | ||||
|  | ||||
|         return super.render(); | ||||
|     } | ||||
|  | ||||
|     entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         if (loadResults.isOptionReloaded("motionEnabled")) { | ||||
|             this.#setMotion(options.is("motionEnabled")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #onMobileResize() { | ||||
|         const currentViewportHeight = getViewportHeight(); | ||||
|         const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight); | ||||
|         this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened); | ||||
|     } | ||||
|  | ||||
|     #setMotion(enabled: boolean) { | ||||
|         document.body.classList.toggle("motion-disabled", !enabled); | ||||
|         jQuery.fx.off = !enabled; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getViewportHeight() { | ||||
|   | ||||
| @@ -7,7 +7,6 @@ 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"> | ||||
| @@ -92,6 +91,8 @@ interface QuickSearchResponse { | ||||
|         noteTitle: string; | ||||
|         notePathTitle: string; | ||||
|         highlightedNotePathTitle: string; | ||||
|         contentSnippet?: string; | ||||
|         highlightedContentSnippet?: string; | ||||
|         attributeSnippet?: string; | ||||
|         highlightedAttributeSnippet?: string; | ||||
|         icon: string; | ||||
| @@ -235,14 +236,24 @@ export default class QuickSearchWidget extends BasicWidget { | ||||
|  | ||||
|                 const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">'); | ||||
|                  | ||||
|                 // 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 | ||||
|                 }); | ||||
|                 // 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>`; | ||||
|                  | ||||
|                 $item.html(itemHtml); | ||||
|                  | ||||
|   | ||||
| @@ -88,6 +88,7 @@ export default function AppearanceSettings() { | ||||
|             <ApplicationTheme /> | ||||
|             {overrideThemeFonts === "true" && <Fonts />} | ||||
|             {isElectron() && <ElectronIntegration /> } | ||||
|             <Performance /> | ||||
|             <MaxContentWidth /> | ||||
|             <RelatedSettings items={[ | ||||
|                 { | ||||
| @@ -245,6 +246,20 @@ function ElectronIntegration() { | ||||
|     ) | ||||
| } | ||||
|  | ||||
| function Performance() { | ||||
|     const [ motionEnabled, setMotionEnabled ] = useTriliumOptionBool("motionEnabled"); | ||||
|  | ||||
|     return <OptionsSection title={t("ui-performance.title")}> | ||||
|         <FormGroup name="motion-enabled"> | ||||
|                 <FormCheckbox | ||||
|                     label={t("ui-performance.enable-motion")} | ||||
|                     currentValue={motionEnabled} onChange={setMotionEnabled} | ||||
|                 /> | ||||
|         </FormGroup> | ||||
|     </OptionsSection> | ||||
| } | ||||
|  | ||||
|  | ||||
| function MaxContentWidth() { | ||||
|     const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth"); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|     <link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest"> | ||||
|     <title>Trilium Notes</title> | ||||
| </head> | ||||
| <body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>"> | ||||
| <body id="trilium-app" class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>"> | ||||
| <noscript><%= t("javascript-required") %></noscript> | ||||
|  | ||||
| <script> | ||||
|   | ||||
| @@ -63,6 +63,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | ||||
|     "dailyBackupEnabled", | ||||
|     "weeklyBackupEnabled", | ||||
|     "monthlyBackupEnabled", | ||||
|     "motionEnabled", | ||||
|     "maxContentWidth", | ||||
|     "compressImages", | ||||
|     "downloadImagesAutomatically", | ||||
|   | ||||
| @@ -152,6 +152,7 @@ const defaultOptions: DefaultOption[] = [ | ||||
|         }, | ||||
|         isSynced: false | ||||
|     }, | ||||
|     { name: "motionEnabled", value: "true", isSynced: false }, | ||||
|  | ||||
|     // Internationalization | ||||
|     { name: "locale", value: "en", isSynced: true }, | ||||
|   | ||||
| @@ -33,6 +33,8 @@ class SearchResult { | ||||
|     score: number; | ||||
|     notePathTitle: string; | ||||
|     highlightedNotePathTitle?: string; | ||||
|     contentSnippet?: string; | ||||
|     highlightedContentSnippet?: string; | ||||
|     attributeSnippet?: string; | ||||
|     highlightedAttributeSnippet?: string; | ||||
|     private fuzzyScore: number; // Track fuzzy score separately | ||||
|   | ||||
| @@ -285,19 +285,15 @@ function performSearch(expression: Expression, searchContext: SearchContext, ena | ||||
|  | ||||
|     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); | ||||
|  | ||||
|     const searchResults = noteSet.notes | ||||
|         .map((note) => { | ||||
|             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); | ||||
|     const searchResults = noteSet.notes.map((note) => { | ||||
|         const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); | ||||
|  | ||||
|             if (!notePathArray) { | ||||
|                 // Log the orphaned note but don't throw - just skip it | ||||
|                 log.info(`Skipping orphaned note without path: ${note.noteId} "${note.title}"`); | ||||
|                 return null; | ||||
|             } | ||||
|         if (!notePathArray) { | ||||
|             throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`); | ||||
|         } | ||||
|  | ||||
|             return new SearchResult(notePathArray); | ||||
|         }) | ||||
|         .filter(result => result !== null) as SearchResult[]; | ||||
|         return new SearchResult(notePathArray); | ||||
|     }); | ||||
|  | ||||
|     for (const res of searchResults) { | ||||
|         res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens, enableFuzzyMatching); | ||||
| @@ -501,12 +497,6 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // If no match found in content, always show the beginning of the note | ||||
|         // This ensures users always get context even when searching by tags/attributes | ||||
|         if (!matchFound) { | ||||
|             snippetStart = 0; | ||||
|         } | ||||
|  | ||||
|         // Extract snippet | ||||
|         let snippet = content.substring(snippetStart, snippetStart + maxLength); | ||||
|          | ||||
| @@ -585,29 +575,11 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // If no matching attributes found but we have attributes, show the first few | ||||
|         // This provides context even when searching didn't match attributes | ||||
|         if (matchingAttributes.length === 0 && attributes.length > 0) { | ||||
|             // Filter out internal attributes if not searching for them | ||||
|             const visibleAttributes = attributes.filter(attr =>  | ||||
|                 !attr.name?.startsWith("internal") &&  | ||||
|                 !attr.name?.startsWith("dateCreated") &&  | ||||
|                 !attr.name?.startsWith("dateModified") | ||||
|             ); | ||||
|              | ||||
|             // Take up to 4 visible attributes | ||||
|             matchingAttributes = visibleAttributes.slice(0, 4).map(attr => ({ | ||||
|                 name: attr.name || "", | ||||
|                 value: attr.value || "", | ||||
|                 type: attr.type || "" | ||||
|             })); | ||||
|         } | ||||
|          | ||||
|         if (matchingAttributes.length === 0) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         // Display attributes inline, separated by spaces | ||||
|         // Limit to 4 lines maximum, similar to content snippet logic | ||||
|         const lines: string[] = []; | ||||
|         for (const attr of matchingAttributes.slice(0, 4)) { | ||||
|             let line = ""; | ||||
| @@ -625,7 +597,7 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let snippet = lines.join(' '); // Join with spaces instead of newlines | ||||
|         let snippet = lines.join('\n'); | ||||
|          | ||||
|         // Apply length limit while preserving line structure | ||||
|         if (snippet.length > maxLength) { | ||||
| @@ -665,8 +637,9 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | ||||
|  | ||||
|     const trimmed = allSearchResults.slice(0, 200); | ||||
|  | ||||
|     // Extract attribute snippets only (content snippets removed for performance) | ||||
|     // Extract content and attribute snippets | ||||
|     for (const result of trimmed) { | ||||
|         result.contentSnippet = extractContentSnippet(result.noteId, searchContext.highlightedTokens); | ||||
|         result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens); | ||||
|     } | ||||
|  | ||||
| @@ -679,6 +652,8 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | ||||
|             noteTitle: title, | ||||
|             notePathTitle: result.notePathTitle, | ||||
|             highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||
|             contentSnippet: result.contentSnippet, | ||||
|             highlightedContentSnippet: result.highlightedContentSnippet, | ||||
|             attributeSnippet: result.attributeSnippet, | ||||
|             highlightedAttributeSnippet: result.highlightedAttributeSnippet, | ||||
|             icon: icon ?? "bx bx-note" | ||||
| @@ -704,9 +679,17 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | ||||
|     for (const result of searchResults) { | ||||
|         result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, ""); | ||||
|          | ||||
|         // Initialize highlighted content snippet | ||||
|         if (result.contentSnippet) { | ||||
|             // Escape HTML but preserve newlines for later conversion to <br> | ||||
|             result.highlightedContentSnippet = escapeHtml(result.contentSnippet); | ||||
|             // Remove any stray < { } that might interfere with our highlighting markers | ||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, ""); | ||||
|         } | ||||
|          | ||||
|         // Initialize highlighted attribute snippet | ||||
|         if (result.attributeSnippet) { | ||||
|             // Escape HTML | ||||
|             // Escape HTML but preserve newlines for later conversion to <br> | ||||
|             result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet); | ||||
|             // Remove any stray < { } that might interfere with our highlighting markers | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, ""); | ||||
| @@ -738,6 +721,16 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 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; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Highlight in attribute snippet | ||||
|             if (result.highlightedAttributeSnippet) { | ||||
|                 const attributeRegex = new RegExp(escapeRegExp(token), "gi"); | ||||
| @@ -755,10 +748,18 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | ||||
|             result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||
|         } | ||||
|          | ||||
|         if (result.highlightedContentSnippet) { | ||||
|             // Replace highlighting markers with HTML tags | ||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||
|             // Convert newlines to <br> tags for HTML display | ||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, "<br>"); | ||||
|         } | ||||
|          | ||||
|         if (result.highlightedAttributeSnippet) { | ||||
|             // Replace highlighting markers with HTML tags | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||
|             // Keep inline display - no conversion of newlines needed | ||||
|             // Convert newlines to <br> tags for HTML display | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/\n/g, "<br>"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -93,6 +93,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi | ||||
|  | ||||
|     // Appearance | ||||
|     splitEditorOrientation: "horziontal" | "vertical"; | ||||
|     motionEnabled: boolean; | ||||
|     codeNoteTheme: string; | ||||
|  | ||||
|     initialized: boolean; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user