Continuing from:
The following .ts script opens multiple web pages in Obsidian WebViewer if the core plugin settings are set for urls to be opened within Obsidian. Otherwise all tabs are opened in the default browser.
- Using CodeScript Toolkit plugin that allows to register pseudo-plugins. A little guide can be found here.
Save this script as Search-web.ts
or any other name in the folder shown in the guide.
import * as obsidian from 'obsidian';
interface Website {
name: string;
url: string;
}
class WebsiteSelectorModal extends obsidian.Modal {
websites: Website[];
originalWebsites: Website[]; // Store the original list
selectedUrls: string[] = [];
searchTerm: string;
filterInput: HTMLInputElement;
listContainer: HTMLElement;
constructor(app: obsidian.App, websites: Website[], searchTerm: string) {
super(app);
this.websites = websites;
this.originalWebsites = [...websites]; // Keep a copy of the original list
this.searchTerm = searchTerm;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
// Create filter input
const filterContainer = contentEl.createEl("div", { cls: "website-filter-container" });
filterContainer.style.padding = "10px";
filterContainer.style.marginBottom = "10px";
const filterLabel = filterContainer.createEl("label", { text: "Filter (type to filter sites): " });
this.filterInput = filterContainer.createEl("input", { type: "text" });
this.filterInput.style.width = "100%";
this.filterInput.style.marginTop = "5px";
// Set focus on the input when modal opens
setTimeout(() => this.filterInput.focus(), 10);
// Handle filter input changes
this.filterInput.addEventListener("input", () => {
this.filterWebsites(this.filterInput.value);
});
// Handle keyboard navigation
this.filterInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && this.websites.length > 0) {
// Select the first visible site on Enter
this.selectedUrls.push(this.websites[0].url);
this.originalWebsites = this.originalWebsites.filter(w => w.name !== this.websites[0].name);
this.websites = this.websites.filter((_, index) => index !== 0);
this.filterInput.value = "";
this.renderWebsiteList();
} else if (e.key === "Escape") {
// Don't close the modal when pressing Escape in the filter input
e.stopPropagation();
this.filterInput.value = "";
this.filterWebsites("");
}
});
// Create website list container
this.listContainer = contentEl.createEl("div", { cls: "website-list-container" });
this.listContainer.style.maxHeight = "300px";
this.listContainer.style.overflow = "auto";
this.renderWebsiteList();
// Create buttons
const buttonContainer = contentEl.createEl("div", { cls: "website-selector-buttons" });
buttonContainer.style.padding = "10px";
buttonContainer.style.textAlign = "center";
const doneButton = buttonContainer.createEl("button", {
text: "Open Selected Sites (or press ESC)",
cls: "mod-cta"
});
doneButton.addEventListener("click", () => this.close());
}
renderWebsiteList() {
this.listContainer.empty();
if (this.websites.length === 0) {
this.listContainer.createEl("div", {
text: "No matching websites found",
cls: "no-results-message"
}).style.padding = "10px";
return;
}
this.websites.forEach(site => {
const row = this.listContainer.createEl("div", {
cls: "website-option"
});
row.style.padding = "8px";
row.style.cursor = "pointer";
row.style.borderBottom = "1px solid var(--background-modifier-border)";
// Highlight the first letter if it matches the filter
const filterText = this.filterInput.value.toLowerCase();
if (filterText && site.name.toLowerCase().startsWith(filterText)) {
const highlightedPart = site.name.substring(0, filterText.length);
const remainingPart = site.name.substring(filterText.length);
const highlight = row.createEl("span", {
text: highlightedPart,
});
highlight.style.fontWeight = "bold";
highlight.style.color = "var(--text-accent)";
row.createEl("span", { text: remainingPart });
} else {
row.createEl("span", { text: site.name });
}
row.addEventListener("click", () => {
this.selectedUrls.push(site.url);
this.originalWebsites = this.originalWebsites.filter(w => w.name !== site.name);
this.websites = this.websites.filter(w => w.name !== site.name);
this.renderWebsiteList();
});
// Add hover effect
row.addEventListener("mouseenter", () => {
row.style.backgroundColor = "var(--background-modifier-hover)";
});
row.addEventListener("mouseleave", () => {
row.style.backgroundColor = "";
});
});
}
filterWebsites(filterText: string) {
if (!filterText) {
this.websites = [...this.originalWebsites];
} else {
const lowerFilter = filterText.toLowerCase();
this.websites = this.originalWebsites.filter(site =>
site.name.toLowerCase().includes(lowerFilter)
);
// Sort to prioritize websites that start with the filter text
this.websites.sort((a, b) => {
const aStartsWith = a.name.toLowerCase().startsWith(lowerFilter);
const bStartsWith = b.name.toLowerCase().startsWith(lowerFilter);
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return a.name.localeCompare(b.name);
});
}
this.renderWebsiteList();
}
onClose() {
const { contentEl } = this;
if (this.selectedUrls.length > 0) {
this.selectedUrls.forEach(url => window.open(url, '_blank'));
}
contentEl.empty();
}
}
const websiteSearch = async (app: obsidian.App): Promise<void> => {
const currentFile = app.workspace.getActiveFile();
if (!currentFile) return;
const editor = app.workspace.activeLeaf?.view?.editor;
if (!editor) return;
const selection = editor.getSelection();
const title = app.metadataCache.getFileCache(currentFile)?.frontmatter?.title || currentFile.basename;
const searchTerm = encodeURIComponent(selection || title);
const websites: Website[] = [
{ name: "Wikipedia", url: `https://en.wikipedia.org/w/index.php?title=${searchTerm}` },
{ name: "Google Wiki", url: `https://www.google.com/search?q=Wikipedia ${searchTerm}` },
{ name: "Archive.org", url: `https://archive.org/search?query=${searchTerm}` },
{ name: "Google", url: `https://www.google.com/search?q=${searchTerm}` },
{ name: "Openreview", url: `https://www.google.com/search?q=site:openreview.net ${searchTerm}` },
{ name: "Arxiv", url: `https://www.google.com/search?q=site:arxiv.org ${searchTerm}` },
{ name: "Google Scholar", url: `https://scholar.google.com/scholar?q=${searchTerm}` },
{ name: "Semantic Scholar", url: `https://www.semanticscholar.org/search?q=${searchTerm}` },
{ name: "Jstor", url: `https://www.jstor.org/action/doAdvancedSearch?q0=${searchTerm}&f0=all&c1=AND&f1=all&acc=on&la=eng+OR+en&so=rel` },
{ name: "Academia.edu", url: `https://www.academia.edu/search?q=${searchTerm}` },
{ name: "LibGen", url: `https://libgen.is/search.php?req=${searchTerm}&lg_topic=libgen&open=0&view=simple&res=25&phrase=1&column=def` }
];
const modal = new WebsiteSelectorModal(app, websites, searchTerm);
modal.open();
};
export class WebsiteSearchPlugin extends obsidian.Plugin {
async onload() {
this.addCommand({
id: 'website-search',
name: 'Website search with user input',
callback: () => websiteSearch(this.app)
});
}
}
export async function invoke(app: obsidian.App): Promise<void> {
return websiteSearch(app);
}
You can add more to the list of websites to be searched.
You can add, for instance:
{ name: "Wiktionary", url: `https://en.wiktionary.org/w/index.php?title=${searchTerm}` },
Just make sure you add the the right amount of tabs (you indent properly) and embed the ${searchTerm}
variable in the expected place. Also, notice the use of commas in the array. The last one doesn’t have a comma. If you break things, Claude.ai or even chatGPT can help with the syntax errors.
You can bind the script to a shortcut. I am using WIN+ALT+I
.
You can search with text already selected or…
…if you don’t have any text selected, the search will be based on the currently open note’s filename.
You can filter in the pop-up menu. If you have a dozen or more websites, then this can be handy. Filtering with go
here:
Then, after clicking the websites you want to search, you can press the button below or press ESC to trigger the search(es).
It works on mobile too, where the default browser will be opened. There is no WebViewer on mobile.