Files
obsidian-excalidraw-plugin/src/dialogs/ScriptInstallPrompt.ts
zsviczian 4ac0a4c565 2.1.8
2024-05-13 22:49:32 +02:00

211 lines
6.7 KiB
TypeScript

import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
import ExcalidrawPlugin from "../main";
import { errorlog, escapeRegExp } from "../utils/Utils";
import { log } from "src/utils/DebugHelper";
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<void> {
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 });
if (!source) {
new Notice(
"Error opening the Excalidraw Script Store page. " +
"Please double check that you can access the website. " +
"I've logged the link in developer console (press CTRL+SHIFT+i)",
5000,
);
log(URL);
this.close();
return;
}
await MarkdownRenderer.render(
this.plugin.app,
source,
this.contentDiv,
"",
this.plugin,
);
this.contentDiv
.querySelectorAll("h1[data-heading],h2[data-heading],h3[data-heading]")
.forEach((el) => {
el.setAttribute("id", el.getAttribute("data-heading"));
});
this.contentDiv.querySelectorAll("a.internal-link").forEach((el) => {
el.removeAttribute("target");
});
} catch (e) {
errorlog({ where: "ScriptInstallPrompt.onOpen", error: e });
new Notice("Could not open ScriptEngine repository");
this.close();
}
}
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(escapeRegExp(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();
}
}