From cbb8f676af5ab0e584c4669244575301f13acf2f Mon Sep 17 00:00:00 2001 From: zsviczian Date: Wed, 9 Aug 2023 19:59:16 +0200 Subject: [PATCH] implemented script store search --- src/dialogs/ScriptInstallPrompt.ts | 160 ++++++++++++++++++++++++++++- styles.css | 26 +++++ 2 files changed, 183 insertions(+), 3 deletions(-) diff --git a/src/dialogs/ScriptInstallPrompt.ts b/src/dialogs/ScriptInstallPrompt.ts index 6668f8b..45bb312 100644 --- a/src/dialogs/ScriptInstallPrompt.ts +++ b/src/dialogs/ScriptInstallPrompt.ts @@ -6,13 +6,74 @@ const URL = "https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md"; export class ScriptInstallPrompt extends Modal { + private contentDiv: HTMLDivElement; constructor(private plugin: ExcalidrawPlugin) { super(plugin.app); // this.titleEl.setText(t("INSTAL_MODAL_TITLE")); } async onOpen(): Promise { + const searchBarWrapper = document.createElement("div"); + searchBarWrapper.classList.add('search-bar-wrapper'); + + + const searchBar = document.createElement("input"); + searchBar.type = "text"; + searchBar.id = "search-bar"; + searchBar.placeholder = "Search..."; + searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count + + const nextButton = document.createElement("button"); + nextButton.textContent = "→"; + nextButton.onclick = () => this.navigateSearchResults("next"); + + const prevButton = document.createElement("button"); + prevButton.textContent = "←"; + prevButton.onclick = () => this.navigateSearchResults("previous"); + + const hitCount = document.createElement("span"); + hitCount.id = "hit-count"; + hitCount.classList.add('hit-count'); + + searchBarWrapper.appendChild(prevButton); + searchBarWrapper.appendChild(nextButton); + searchBarWrapper.appendChild(searchBar); + searchBarWrapper.appendChild(hitCount); + + this.contentEl.prepend(searchBarWrapper); + + searchBar.addEventListener("input", (e) => { + this.clearHighlights(); + const searchTerm = (e.target as HTMLInputElement).value; + + if (searchTerm && searchTerm.length > 0) { + this.highlightSearchTerm(searchTerm); + const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length; + hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : ""; + setTimeout(()=>this.navigateSearchResults("next")); + } else { + hitCount.textContent = ""; + } + }); + + + searchBar.addEventListener("keydown", (e) => { + // If Ctrl/Cmd + F is pressed, focus on search bar + if ((e.ctrlKey || e.metaKey) && e.key === "f") { + e.preventDefault(); + searchBar.focus(); + } + // If Enter is pressed, navigate to next result + else if (e.key === "Enter") { + e.preventDefault(); + this.navigateSearchResults(e.shiftKey ? "previous" : "next"); + } + }); + this.contentEl.classList.add("excalidraw-scriptengine-install"); + this.contentDiv = document.createElement("div"); + this.contentEl.appendChild(this.contentDiv); + this.containerEl.classList.add("excalidraw-scriptengine-install"); try { const source = await request({ url: URL }); @@ -29,16 +90,16 @@ export class ScriptInstallPrompt extends Modal { } await MarkdownRenderer.renderMarkdown( source, - this.contentEl, + this.contentDiv, "", this.plugin, ); - this.contentEl + this.contentDiv .querySelectorAll("h1[data-heading],h2[data-heading],h3[data-heading]") .forEach((el) => { el.setAttribute("id", el.getAttribute("data-heading")); }); - this.contentEl.querySelectorAll("a.internal-link").forEach((el) => { + this.contentDiv.querySelectorAll("a.internal-link").forEach((el) => { el.removeAttribute("target"); }); } catch (e) { @@ -48,6 +109,99 @@ export class ScriptInstallPrompt extends Modal { } } + highlightSearchTerm(searchTerm: string): void { + // Create a walker to traverse text nodes + const walker = document.createTreeWalker( + this.contentDiv, + NodeFilter.SHOW_TEXT, + { + acceptNode: (node: Text) => { + return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ? + NodeFilter.FILTER_ACCEPT : + NodeFilter.FILTER_REJECT; + } + } + ); + + const nodesToReplace: Text[] = []; + while (walker.nextNode()) { + nodesToReplace.push(walker.currentNode as Text); + } + + nodesToReplace.forEach(node => { + const nodeContent = node.nodeValue!; + const newNode = document.createDocumentFragment(); + + let lastIndex = 0; + let match; + const regex = new RegExp(searchTerm, 'gi'); + + // Iterate over all matches in the text node + while ((match = regex.exec(nodeContent)) !== null) { + const before = document.createTextNode(nodeContent.slice(lastIndex, match.index)); + const highlighted = document.createElement('mark'); + highlighted.className = 'search-highlight'; + highlighted.textContent = match[0]; + highlighted.classList.add('search-result'); + + newNode.appendChild(before); + newNode.appendChild(highlighted); + + lastIndex = regex.lastIndex; + } + newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex))); + node.replaceWith(newNode); + }); + } + + + clearHighlights(): void { + this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => { + el.outerHTML = el.innerHTML; + }); + } + + navigateSearchResults(direction: "next" | "previous"): void { + const highlights: HTMLElement[] = Array.from( + this.contentDiv.querySelectorAll("mark.search-highlight") + ); + + if (highlights.length === 0) return; + + const currentActiveIndex = highlights.findIndex((highlight) => + highlight.classList.contains("active-highlight") + ); + + if (currentActiveIndex !== -1) { + highlights[currentActiveIndex].classList.remove("active-highlight"); + highlights[currentActiveIndex].style.border = "none"; + } + + let nextActiveIndex = 0; + if (direction === "next") { + nextActiveIndex = + currentActiveIndex === highlights.length - 1 + ? 0 + : currentActiveIndex + 1; + } else if (direction === "previous") { + nextActiveIndex = + currentActiveIndex === 0 + ? highlights.length - 1 + : currentActiveIndex - 1; + } + + const nextActiveHighlight = highlights[nextActiveIndex]; + nextActiveHighlight.classList.add("active-highlight"); + nextActiveHighlight.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + + // Update the hit count + const hitCount = document.getElementById("hit-count"); + hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`; + } + onClose(): void { this.contentEl.empty(); } diff --git a/styles.css b/styles.css index 98bcd56..3e15a40 100644 --- a/styles.css +++ b/styles.css @@ -381,4 +381,30 @@ div.excalidraw-draginfo { .excalidraw-image-wrapper img { margin: auto; +} + +.modal-content.excalidraw-scriptengine-install .search-bar-wrapper { + position: sticky; + top: 1em; + margin-right: 1em; + display: flex; + align-items: center; + gap: 5px; + flex-wrap: nowrap; + z-index: 10; + background: var(--background-secondary); + padding: 0.5em; + border-bottom: 1px solid var(--background-modifier-border); + float: right; + max-width: 28em; +} + +.modal-content.excalidraw-scriptengine-install .hit-count { + margin-left: 0.5em; + white-space: nowrap; +} + +.modal-content.excalidraw-scriptengine-install .active-highlight { + border: 2px solid var(--color-accent-2); + background-color: var(--color-accent); } \ No newline at end of file