diff --git a/Base/res/ladybird/about-pages/bookmarks.html b/Base/res/ladybird/about-pages/bookmarks.html index 3738a88ea2..369e3fb551 100644 --- a/Base/res/ladybird/about-pages/bookmarks.html +++ b/Base/res/ladybird/about-pages/bookmarks.html @@ -225,6 +225,58 @@ font-size: 12px; min-width: 0; } + + .search-container { + display: flex; + align-items: center; + gap: 8px; + + max-width: 65%; + margin: 0 auto 24px auto; + padding: 0px 12px; + + background-color: var(--tree-hover-color); + border: none; + border-radius: 8px; + } + + .search-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + color: var(--empty-text-color); + } + + .search-input { + flex-grow: 1; + min-width: 0; + font-size: 13px; + color: inherit; + + background: none; + border: none; + outline: none; + padding: 0; + } + + .search-input::placeholder { + color: var(--empty-text-color); + } + + .search-highlight { + color: inherit; + background-color: var(--tree-selected-color); + border-radius: 2px; + } + + .search-no-results { + color: var(--empty-text-color); + + padding: 40px 20px; + + text-align: center; + font-size: 14px; + } @@ -250,6 +302,21 @@ +
+ + + + + +
+
@@ -276,6 +343,7 @@ let BOOKMARKS = []; let EXPANDED_FOLDERS = new Set(); let DRAGGED_ITEM_ID = null; + let SEARCH_QUERY = ""; const DROP_INDICATOR = document.createElement("div"); DROP_INDICATOR.className = "drop-indicator"; @@ -315,6 +383,89 @@ return createElement("span", { className: "item-info" }, [title, url]); } + function createHighlightedBookmarkInfo(item) { + const link = createElement("a", { href: item.url }); + link.append(...highlightText(item.title || item.url, SEARCH_QUERY)); + + const title = createElement("span", { className: "item-title" }, [link]); + + const url = createElement("span", { className: "item-url" }); + url.append(...highlightText(item.url, SEARCH_QUERY)); + + return createElement("span", { className: "item-info" }, [title, url]); + } + + function itemMatchesQuery(item, query) { + const lowerQuery = query.toLowerCase(); + const title = (item.title || "").toLowerCase(); + + if (item.type === "bookmark") { + const url = (item.url || "").toLowerCase(); + return title.includes(lowerQuery) || url.includes(lowerQuery); + } + + return title.includes(lowerQuery); + } + + function filterBookmarks(items, query) { + if (!query) { + return items; + } + + const result = []; + + for (const item of items) { + if (item.type === "bookmark") { + if (itemMatchesQuery(item, query)) { + result.push(item); + } + } else if (item.type === "folder") { + const filteredChildren = filterBookmarks(item.children || [], query); + const folderMatches = itemMatchesQuery(item, query); + + if (filteredChildren.length > 0) { + result.push({ ...item, children: filteredChildren }); + } else if (folderMatches) { + result.push({ ...item, children: [] }); + } + } + } + + return result; + } + + function highlightText(text, query) { + if (!query) { + return [document.createTextNode(text)]; + } + + const lowerText = text.toLowerCase(); + const lowerQuery = query.toLowerCase(); + const fragments = []; + let lastIndex = 0; + let matchIndex = lowerText.indexOf(lowerQuery, lastIndex); + + for (; matchIndex !== -1; matchIndex = lowerText.indexOf(lowerQuery, lastIndex)) { + if (matchIndex > lastIndex) { + fragments.push(document.createTextNode(text.slice(lastIndex, matchIndex))); + } + + const mark = createElement("mark", { + className: "search-highlight", + textContent: text.slice(matchIndex, matchIndex + query.length), + }); + fragments.push(mark); + + lastIndex = matchIndex + query.length; + } + + if (lastIndex < text.length) { + fragments.push(document.createTextNode(text.slice(lastIndex))); + } + + return fragments.length > 0 ? fragments : [document.createTextNode(text)]; + } + function toggleFolder(folderId) { if (EXPANDED_FOLDERS.has(folderId)) { EXPANDED_FOLDERS.delete(folderId); @@ -386,6 +537,12 @@ } function setupDragHandlers(row, item, parentId, index) { + if (SEARCH_QUERY) { + row.draggable = false; + row.style.cursor = item.type === "folder" ? "pointer" : "default"; + return; + } + row.draggable = true; row.addEventListener("dragstart", event => { @@ -455,7 +612,23 @@ return; } - renderItems(bookmarkTreeContainer, BOOKMARKS, null); + if (SEARCH_QUERY) { + const filteredItems = filterBookmarks(BOOKMARKS, SEARCH_QUERY); + + if (filteredItems.length === 0) { + bookmarkTreeContainer.appendChild( + createElement("div", { + className: "search-no-results", + textContent: "No bookmarks matching your search.", + }) + ); + return; + } + + renderItems(bookmarkTreeContainer, filteredItems, null); + } else { + renderItems(bookmarkTreeContainer, BOOKMARKS, null); + } } function renderItems(container, items, parentId) { @@ -477,7 +650,7 @@ function renderBookmarkRow(row, item, index, parentId) { row.appendChild(createSpacer()); row.appendChild(createFaviconIcon(item.favicon)); - row.appendChild(createBookmarkInfo(item)); + row.appendChild(SEARCH_QUERY ? createHighlightedBookmarkInfo(item) : createBookmarkInfo(item)); setupDragHandlers(row, item, parentId, index); addContextMenuHandler(row, item, parentId || null); @@ -485,7 +658,8 @@ function renderFolderRow(row, wrapper, item, index, parentId) { const hasChildren = item.children && item.children.length > 0; - const isExpanded = hasChildren && EXPANDED_FOLDERS.has(item.id); + const isSearching = SEARCH_QUERY.length > 0; + const isExpanded = hasChildren && (isSearching || EXPANDED_FOLDERS.has(item.id)); if (hasChildren) { row.classList.add("expandable"); @@ -501,12 +675,19 @@ } row.appendChild(createIcon(FOLDER_SVG)); - row.appendChild( - createElement("span", { - className: "item-title", - textContent: item.title || "(no title)", - }) - ); + + if (isSearching) { + const folderTitle = createElement("span", { className: "item-title" }); + folderTitle.append(...highlightText(item.title || "(no title)", SEARCH_QUERY)); + row.appendChild(folderTitle); + } else { + row.appendChild( + createElement("span", { + className: "item-title", + textContent: item.title || "(no title)", + }) + ); + } setupDragHandlers(row, item, parentId, index); addContextMenuHandler(row, item, item.id); @@ -713,6 +894,13 @@ return html; } + const searchInput = document.getElementById("search-input"); + + searchInput.addEventListener("input", () => { + SEARCH_QUERY = searchInput.value.trim(); + renderTree(); + }); + exportButton.addEventListener("click", () => { closeMenu();