first commit

This commit is contained in:
fnsign 2026-05-09 14:53:31 +02:00
commit 94d987466f
4 changed files with 1000 additions and 0 deletions

149
README.md Normal file
View file

@ -0,0 +1,149 @@
# Embedded-Omnisearch
An Obsidian plugin that renders a compact, inline search UI directly inside a note. It is powered by the [Omnisearch](https://github.com/scambier/obsidian-omnisearch) API and includes plugin settings for default page size and highlight appearance.
## Features
- Inline vault search inside any note via a fenced code block.
- Automatically switches notes containing an embedded search block into preview mode when needed.
- Accent-insensitive term matching with configurable highlight color and opacity.
- Paginated results with configurable page size.
- Results rendered as a compact table with file name, relevance score, and excerpt preview.
- Opens clicked results in preview mode.
- Keyboard shortcuts: `Enter` to search, `Escape` to clear.
- Clear button with dynamic visibility.
- Hold `Alt` over a result row to show the full file path as a popover.
- Debounced search input (350 ms).
- Plugin settings page for default results per page, highlight color, and highlight opacity.
- Code-block-level `pageSize` override that takes precedence over the global default.
- Proper lifecycle management with registered views refreshed when settings change.
## Requirements
- [Obsidian](https://obsidian.md/) v1.0.0 or later.
- [Omnisearch](https://github.com/scambier/obsidian-omnisearch) community plugin, installed and enabled.
## Installation
1. Copy this folder (`manifest.json`, `main.js`, `styles.css`) into your vault at:
```
<vault>/.obsidian/plugins/embedded-omnisearch/
```
2. In Obsidian, open **Settings -> Community plugins** and enable **Embedded-Omnisearch**.
3. Make sure Omnisearch is also enabled and has indexed your vault.
4. Open **Settings -> Community plugins -> Embedded-Omnisearch** to configure defaults.
## Usage
Add a fenced code block with the language identifier `embedded-omnisearch` to any note:
````
```embedded-omnisearch
```
````
Open the note in **Reading View** or **Live Preview**. The code block is replaced by a search field.
If you open a markdown note containing an `embedded-omnisearch` code block, the plugin will also try to switch that leaf to preview mode automatically so the embedded UI becomes visible.
### Configuration
You can set options inside the code block. Currently supported:
| Option | Default | Description |
| ---------- | ------- | ---------------------------------- |
| `pageSize` | Plugin setting | Number of search results per page. |
Example:
````
```embedded-omnisearch
pageSize: 20
```
````
### Plugin Settings
The plugin settings page provides defaults for the embedded search UI:
| Setting | Default | Description |
| ------- | ------- | ----------- |
| `Results per page` | `10` | Default page size for embedded searches. |
| `Highlight color` | `#cca300` | Base color used for highlighted matches. |
| `Highlight opacity` | `35%` | Opacity of the highlight background. |
## How It Works
1. The plugin registers a **Markdown code block processor** for the language `embedded-omnisearch`.
2. When Obsidian renders such a block, the plugin creates a `SearchView` component attached to the block's DOM element.
3. The plugin watches active markdown leaves and switches notes containing an embedded search block to preview mode when needed.
4. On each query the view calls `globalThis.omnisearch.search(query)` and renders the results as a styled table.
5. Each result shows the linked file name, a rounded relevance score, and an excerpt with accent-insensitive highlighted matches.
6. Highlight color and opacity are exposed through plugin settings and applied via the `--eo-highlight-color` CSS variable.
7. Results are split into pages; `<` / `>` buttons navigate between pages.
8. Clicking a result link opens the file in preview mode.
9. Holding `Alt` while hovering a result row displays the full vault path in a small popover.
10. All DOM event listeners use Obsidian's `registerDomEvent` and are cleaned up when the component unloads.
## Customization
### Styles
The plugin ships its own `styles.css` using Obsidian CSS variables (`--background-secondary`, `--text-normal`, `--text-muted`, `--background-modifier-border`, etc.), so it adapts to any theme automatically.
You can override any class in an Obsidian CSS snippet. Key classes:
- `.eo-wrap` — outer wrapper
- `.eo-input-wrap` — search field container
- `.omnisearch-input-field` — search input field
- `.eo-clear` — clear button
- `.eo-status` — status / result count line
- `.eo-results` — results container
- `.eo-results-table` — result table
- `.eo-results-row` / `.eo-results-cell` — result rows and cells
- `.eo-results-link` — clickable note link
- `.eo-page-bar` / `.eo-page-btn` / `.eo-page-info` — pagination
- `.eo-popover` — Alt+hover path popover
### Highlight Appearance
The plugin applies highlight color and opacity through the `--eo-highlight-color` CSS variable, which is updated from the plugin settings page. You can still override that variable in a custom CSS snippet if needed.
## Troubleshooting
### The code block shows raw text instead of the search UI
- Make sure the plugin is enabled in **Settings → Community plugins**.
- The note must be in **Reading View** or **Live Preview**. The plugin will try to switch notes with embedded search blocks out of Source mode automatically, but it can only do that for normal markdown leaves.
### "Omnisearch API not available"
- Omnisearch must be installed and enabled.
- Wait for Obsidian to finish loading all plugins on startup.
### Some notes do not appear in results
- Omnisearch indexing may not be complete yet.
- Check Omnisearch exclusion settings.
- The notes must actually contain the searched terms.
### Path popover does not appear
- Hold the `Alt` key while hovering over a result row.
- Results must already be rendered.
## Files
| File | Purpose |
| --------------- | -------------------------------------------------------------- |
| `manifest.json` | Plugin metadata (id, version, description). |
| `main.js` | Plugin code: embedded search UI, settings, highlighting, pagination, preview enforcement, and popover behavior. |
| `styles.css` | CSS using Obsidian variables for automatic theme compatibility. |
## License
MIT

626
main.js Normal file
View file

@ -0,0 +1,626 @@
'use strict';
var obsidian = require('obsidian');
/* === Pre-compiled patterns === */
var RE_HTML = /[&<>"']/g;
var HTML_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
var RE_DIACRITICS = /[\u0300-\u036f]/g;
var RE_WS = /\s+/;
var RE_EMBEDDED_BLOCK = /^```embedded-omnisearch(?:\s|$)/m;
var DEFAULT_SETTINGS = { pageSize: 10, highlightColor: "#cca300", highlightOpacity: 0.35 };
/* === Helpers === */
function escHtml(s) { return String(s).replace(RE_HTML, function (c) { return HTML_MAP[c]; }); }
function norm(s) {
return String(s == null ? "" : s).trim().normalize("NFD")
.replace(RE_DIACRITICS, "").replace(/ß/g, "ss").toLowerCase();
}
function bname(p) { return String(p || "").replace(/^.*[\\/]/, ""); }
function getApi() {
var a = globalThis.omnisearch;
return a && typeof a.search === "function" ? a : null;
}
function clampPageSize(value) {
var n = parseInt(value, 10);
if (!isFinite(n) || isNaN(n)) return DEFAULT_SETTINGS.pageSize;
if (n < 1) return 1;
if (n > 100) return 100;
return n;
}
function normalizeHexColor(value) {
var color = String(value == null ? "" : value).trim();
if (/^#[0-9a-fA-F]{6}$/.test(color)) return color.toLowerCase();
if (/^#[0-9a-fA-F]{3}$/.test(color)) {
return ("#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3]).toLowerCase();
}
return DEFAULT_SETTINGS.highlightColor;
}
function clampOpacity(value) {
var n = parseFloat(value);
if (!isFinite(n) || isNaN(n)) return DEFAULT_SETTINGS.highlightOpacity;
if (n < 0) return 0;
if (n > 1) return 1;
return Math.round(n * 100) / 100;
}
function hexToRgba(hex, alpha) {
var color = normalizeHexColor(hex);
var r = parseInt(color.slice(1, 3), 16);
var g = parseInt(color.slice(3, 5), 16);
var b = parseInt(color.slice(5, 7), 16);
return "rgba(" + r + "," + g + "," + b + "," + alpha + ")";
}
/* === Accent-insensitive fold === */
function foldWithMap(str) {
var src = String(str), starts = [], ends = [], folded = "", i = 0;
for (var ch of src) {
var start = i, end = (i += ch.length);
var n = ch.normalize("NFD");
for (var j = 0; j < n.length; j++) {
var code = n.charCodeAt(j);
if (code >= 0x0300 && code <= 0x036f) continue;
if (code === 0xDF) {
folded += "ss"; starts.push(start, start); ends.push(end, end);
} else {
folded += n[j].toLowerCase(); starts.push(start); ends.push(end);
}
}
}
return { f: folded, s: starts, e: ends, o: src };
}
var stripEl = document.createElement("span");
function highlight(html, terms) {
stripEl.innerHTML = String(html != null ? html : "");
var plain = stripEl.textContent || "";
if (!plain) return "";
var m = foldWithMap(plain), f = m.f, s = m.s, e = m.e, o = m.o;
if (!f || !terms.length) return plain;
var ranges = [];
for (var t of terms) {
if (!t) continue;
var pos = 0, idx;
while ((idx = f.indexOf(t, pos)) >= 0) {
ranges.push([s[idx], e[idx + t.length - 1]]);
pos = idx + t.length;
}
}
if (!ranges.length) return plain;
ranges.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1]; });
var merged = [ranges[0]];
for (var i = 1; i < ranges.length; i++) {
var last = merged[merged.length - 1], a = ranges[i][0], b = ranges[i][1];
if (a > last[1]) merged.push([a, b]);
else last[1] = Math.max(last[1], b);
}
var out = "", cur = 0;
for (var r of merged) {
out += escHtml(o.slice(cur, r[0]));
out += '<mark class="omnisearch-highlight" style="background-color:var(--eo-highlight-color, rgba(204,163,0,0.35));color:var(--text-normal);padding:0 .15em;border-radius:3px">'
+ escHtml(o.slice(r[0], r[1])) + '</mark>';
cur = r[1];
}
var span = document.createElement("span");
span.innerHTML = out + escHtml(o.slice(cur));
return span;
}
/* === Plugin === */
class EmbeddedOmnisearchPlugin extends obsidian.Plugin {
async onload() {
this.previewCheckToken = 0;
this.views = new Set();
await this.loadSettings();
this.applyHighlightColor();
this.addSettingTab(new EmbeddedOmnisearchSettingTab(this.app, this));
this.registerEvent(this.app.workspace.on("active-leaf-change", (leaf) => {
this.enforceLeafPreview(leaf);
}));
this.app.workspace.onLayoutReady(() => {
this.enforceLeafPreview(this.app.workspace.activeLeaf);
});
this.registerMarkdownCodeBlockProcessor("embedded-omnisearch", (source, el, ctx) => {
ctx.addChild(new SearchView(this, el, this.parseConfig(source)));
});
}
onunload() {
if (typeof document !== "undefined" && document.documentElement) {
document.documentElement.style.removeProperty("--eo-highlight-color");
}
if (this.views) this.views.clear();
}
async loadSettings() {
var data = {};
try {
data = await this.loadData() || {};
} catch (e) {}
this.settings = {
pageSize: clampPageSize(data.pageSize),
highlightColor: normalizeHexColor(data.highlightColor),
highlightOpacity: clampOpacity(data.highlightOpacity)
};
}
async saveSettings() {
await this.saveData(this.settings);
this.applyHighlightColor();
this.refreshViews();
}
applyHighlightColor() {
if (typeof document === "undefined" || !document.documentElement) return;
document.documentElement.style.setProperty("--eo-highlight-color", hexToRgba(this.settings.highlightColor, this.settings.highlightOpacity));
}
refreshViews() {
if (!this.views) return;
for (var view of this.views) {
if (view && typeof view.onSettingsChanged === "function") view.onSettingsChanged();
}
}
registerSearchView(view) {
this.views.add(view);
}
unregisterSearchView(view) {
this.views.delete(view);
}
getLeafMarkdown(leaf) {
if (!leaf || !leaf.view || leaf.view.getViewType() !== "markdown") return "";
if (typeof leaf.view.getViewData === "function") {
try {
return String(leaf.view.getViewData() || "");
} catch (e) {}
}
if (typeof leaf.view.data === "string") return leaf.view.data;
return "";
}
noteContainsEmbeddedSearch(text) {
return RE_EMBEDDED_BLOCK.test(String(text || ""));
}
setLeafPreview(leaf) {
if (!leaf || !leaf.view || leaf.view.getViewType() !== "markdown") return false;
var vs = leaf.getViewState ? leaf.getViewState() || {} : {};
if (vs.state && vs.state.mode === "preview") return false;
leaf.setViewState({
type: vs.type || "markdown",
state: Object.assign({}, vs.state || {}, { mode: "preview" })
});
return true;
}
async enforceLeafPreview(leaf) {
if (!leaf || !leaf.view || leaf.view.getViewType() !== "markdown") return;
var file = leaf.view.file;
if (!file) return;
var markdown = this.getLeafMarkdown(leaf);
if (this.noteContainsEmbeddedSearch(markdown)) {
this.setLeafPreview(leaf);
return;
}
var token = ++this.previewCheckToken;
var cached = "";
try {
cached = await this.app.vault.cachedRead(file);
} catch (e) {
return;
}
if (token !== this.previewCheckToken) return;
if (!this.noteContainsEmbeddedSearch(cached)) return;
this.setLeafPreview(leaf);
}
parseConfig(source) {
var cfg = { pageSize: this.settings.pageSize, hasPageSizeOverride: false };
for (var line of (source || "").split("\n")) {
var m = line.match(/^\s*pageSize\s*:\s*(\d+)/);
if (m) {
cfg.pageSize = clampPageSize(m[1]);
cfg.hasPageSizeOverride = true;
}
}
return cfg;
}
}
/* === Search View Component === */
class SearchView extends obsidian.Component {
constructor(plugin, el, cfg) {
super();
this.plugin = plugin;
this.app = plugin.app;
this.rootEl = el;
this.cfg = cfg;
this.results = [];
this.page = 0;
this.pageSize = this.getPageSize();
this.terms = [];
this.timer = null;
this.altHeld = false;
this.hoverRow = null;
this.mx = 0;
this.my = 0;
}
onload() {
this.plugin.registerSearchView(this);
this.build();
this.bind();
}
onunload() {
clearTimeout(this.timer);
if (this.popover && this.popover.parentNode) this.popover.remove();
this.plugin.unregisterSearchView(this);
}
getPageSize() {
if (this.cfg && this.cfg.hasPageSizeOverride) return clampPageSize(this.cfg.pageSize);
return clampPageSize(this.plugin.settings.pageSize);
}
onSettingsChanged() {
var nextPageSize = this.getPageSize();
if (nextPageSize === this.pageSize) return;
this.pageSize = nextPageSize;
if (!this.results.length) return;
var total = Math.ceil(this.results.length / this.pageSize);
if (this.page > total - 1) this.page = Math.max(0, total - 1);
this.renderPage();
}
/* --- Build UI --- */
build() {
var root = this.rootEl;
root.empty();
root.addClass("omnisearch-modal", "eo-wrap");
/* Input */
var inputWrap = root.createDiv({ cls: "omnisearch-input-container eo-input-wrap" });
this.input = inputWrap.createEl("input", {
cls: "omnisearch-input-field",
attr: { type: "text", placeholder: "Find with Omnisearch...", spellcheck: "false", autocomplete: "off" }
});
this.input.style.fontStyle = "italic";
this.clearBtn = inputWrap.createEl("button", {
cls: "eo-clear",
text: "\u2715",
attr: { type: "button", "aria-label": "clear" }
});
this.clearBtn.style.opacity = "0";
this.clearBtn.style.pointerEvents = "none";
/* Status */
this.statusEl = root.createDiv({ cls: "eo-status" });
/* Results list */
this.resultsEl = root.createDiv({ cls: "eo-results" });
/* Pagination */
this.pageBar = root.createDiv({ cls: "eo-page-bar" });
this.pageBar.style.display = "none";
this.prevBtn = this.pageBar.createEl("button", { cls: "eo-page-btn", text: "<", attr: { type: "button" } });
this.pageInfo = this.pageBar.createEl("span", { cls: "eo-page-info" });
this.nextBtn = this.pageBar.createEl("button", { cls: "eo-page-btn", text: ">", attr: { type: "button" } });
/* Popover */
this.popover = createEl("div", { cls: "eo-popover" });
this.popover.style.display = "none";
document.body.appendChild(this.popover);
}
/* --- Bind events --- */
bind() {
/* Input */
this.registerDomEvent(this.input, "input", () => {
var v = this.input.value.trim();
var has = !!v;
this.clearBtn.style.opacity = has ? "1" : "0";
this.clearBtn.style.pointerEvents = has ? "auto" : "none";
this.input.classList.toggle("has-value", has);
this.input.style.fontStyle = has ? "normal" : "italic";
clearTimeout(this.timer);
if (!has) {
this.statusEl.textContent = "";
this.resultsEl.empty();
this.results = [];
this.page = 0;
this.pageBar.style.display = "none";
return;
}
this.timer = setTimeout(() => this.search(this.input.value), 350);
});
this.registerDomEvent(this.input, "keydown", (e) => {
if (e.key === "Enter") this.search(this.input.value);
if (e.key === "Escape") this.clearAll();
});
/* Clear */
this.registerDomEvent(this.clearBtn, "click", (e) => {
e.preventDefault();
this.clearAll();
});
/* Pagination */
this.registerDomEvent(this.prevBtn, "click", (e) => {
e.preventDefault();
if (this.page > 0) { this.page--; this.renderPage(); }
});
this.registerDomEvent(this.nextBtn, "click", (e) => {
e.preventDefault();
var total = Math.ceil(this.results.length / this.pageSize);
if (this.page < total - 1) { this.page++; this.renderPage(); }
});
/* Result clicks — open in preview */
this.registerDomEvent(this.resultsEl, "click", async (ev) => {
var a = ev.target.closest("a.internal-link");
if (!a) return;
ev.preventDefault();
var href = a.dataset.href || a.getAttribute("href");
if (!href) return;
var file = this.app.vault.getAbstractFileByPath(href);
if (!file) return;
var leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file, { state: { mode: "preview" } });
});
/* Popover — mousemove / mouseleave on results */
this.registerDomEvent(this.resultsEl, "mousemove", (ev) => {
this.mx = ev.clientX;
this.my = ev.clientY;
this.hoverRow = ev.target.closest(".eo-results-row") || null;
this.showPopover();
});
this.registerDomEvent(this.resultsEl, "mouseleave", () => {
this.hoverRow = null;
this.popover.style.display = "none";
});
/* Alt key — global */
this.registerDomEvent(document, "keydown", (e) => {
if (e.key === "Alt") { this.altHeld = true; this.showPopover(); }
});
this.registerDomEvent(document, "keyup", (e) => {
if (e.key === "Alt") { this.altHeld = false; this.popover.style.display = "none"; }
});
}
/* --- Popover --- */
showPopover() {
var fp = this.hoverRow && this.hoverRow.dataset && this.hoverRow.dataset.filepath;
if (!fp || !this.altHeld) { this.popover.style.display = "none"; return; }
this.popover.textContent = fp;
this.popover.style.display = "block";
this.popover.style.left = (this.mx + 14) + "px";
this.popover.style.top = (this.my + 14) + "px";
}
/* --- Clear --- */
clearAll() {
this.input.value = "";
this.statusEl.textContent = "";
this.resultsEl.empty();
this.results = [];
this.page = 0;
this.terms = [];
this.pageBar.style.display = "none";
this.clearBtn.style.opacity = "0";
this.clearBtn.style.pointerEvents = "none";
this.input.classList.remove("has-value");
this.input.style.fontStyle = "italic";
this.input.focus();
}
/* --- Search --- */
async search(raw) {
var q = String(raw || "").trim();
this.resultsEl.empty();
this.pageBar.style.display = "none";
this.results = [];
this.page = 0;
this.terms = [];
if (!q) { this.statusEl.textContent = ""; return; }
var api = getApi();
if (!api) { this.statusEl.textContent = "\u274c Omnisearch API not available"; return; }
this.statusEl.textContent = "Searching\u2026";
var res = await api.search(q);
if (!res || !res.length) { this.statusEl.textContent = 'No results for "' + q + '"'; return; }
this.results = res;
this.pageSize = this.getPageSize();
this.terms = q.split(RE_WS).filter(Boolean).map(norm);
this.page = 0;
this.renderPage();
}
/* --- Render one page --- */
renderPage() {
this.resultsEl.empty();
var ps = this.pageSize;
var total = Math.ceil(this.results.length / ps);
var start = this.page * ps;
var items = this.results.slice(start, start + ps);
if (!items.length) return;
this.statusEl.textContent = this.results.length + " results \u2014 page " + (this.page + 1) + " of " + total;
var tbl = this.resultsEl.createEl("table", { cls: "eo-results-table" });
var colgroup = tbl.createEl("colgroup");
colgroup.createEl("col", { cls: "eo-col-file" });
colgroup.createEl("col", { cls: "eo-col-score" });
colgroup.createEl("col", { cls: "eo-col-preview" });
var thead = tbl.createEl("thead", { cls: "eo-results-head" });
var headRow = thead.createEl("tr", { cls: "eo-results-head-row" });
headRow.createEl("th", { cls: "eo-results-head-cell eo-results-head-file", text: "File" });
headRow.createEl("th", { cls: "eo-results-head-cell eo-results-head-score", text: "Score" });
headRow.createEl("th", { cls: "eo-results-head-cell eo-results-head-preview", text: "Preview" });
var tbody = tbl.createEl("tbody", { cls: "eo-results-body" });
for (var r of items) {
var tr = tbody.createEl("tr", { cls: "eo-results-row" });
tr.dataset.filepath = r.path || "";
var tdFile = tr.createEl("td", { cls: "eo-results-cell eo-results-file" });
tdFile.createEl("a", {
cls: "internal-link eo-results-link",
text: bname(r.path),
attr: { "data-href": r.path, href: r.path }
});
tr.createEl("td", {
cls: "eo-results-cell eo-results-score",
text: String(Math.round(r.score || 0))
});
var tdBody = tr.createEl("td", { cls: "eo-results-cell eo-results-preview" });
var uniq = Array.from(new Set(
this.terms.concat((r.foundWords || []).map(norm)).filter(Boolean)
)).sort(function (a, b) { return b.length - a.length; });
var hl = highlight(r.excerpt, uniq);
if (typeof hl === "string") tdBody.textContent = hl;
else tdBody.appendChild(hl);
}
if (total > 1) {
this.pageBar.style.display = "flex";
this.prevBtn.disabled = this.page === 0;
this.prevBtn.style.opacity = this.page === 0 ? "0.35" : "1";
this.nextBtn.disabled = this.page >= total - 1;
this.nextBtn.style.opacity = this.page >= total - 1 ? "0.35" : "1";
this.pageInfo.textContent = (this.page + 1) + " / " + total;
} else {
this.pageBar.style.display = "none";
}
}
}
class EmbeddedOmnisearchSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
var containerEl = this.containerEl;
containerEl.empty();
containerEl.createEl("h2", { text: "Embedded-Omnisearch Settings" });
new obsidian.Setting(containerEl)
.setName("Results per page")
.setDesc("Default pageSize value. A code block with pageSize: ... overrides this setting.")
.addText((text) => {
var commitPageSize = async () => {
var normalized = clampPageSize(text.getValue());
if (text.getValue() !== String(normalized)) text.setValue(String(normalized));
if (this.plugin.settings.pageSize === normalized) return;
this.plugin.settings.pageSize = normalized;
await this.plugin.saveSettings();
};
text.setPlaceholder(String(DEFAULT_SETTINGS.pageSize));
text.setValue(String(this.plugin.settings.pageSize));
text.inputEl.type = "number";
text.inputEl.min = "1";
text.inputEl.max = "100";
text.inputEl.step = "1";
text.inputEl.addEventListener("change", commitPageSize);
text.inputEl.addEventListener("blur", commitPageSize);
});
var colorSetting = new obsidian.Setting(containerEl)
.setName("Highlight color")
.setDesc("Color used to highlight matching terms.");
if (typeof colorSetting.addColorPicker === "function") {
colorSetting.addColorPicker((picker) => {
picker.setValue(this.plugin.settings.highlightColor);
picker.onChange(async (value) => {
var normalized = normalizeHexColor(value);
if (normalized !== value) picker.setValue(normalized);
if (this.plugin.settings.highlightColor === normalized) return;
this.plugin.settings.highlightColor = normalized;
await this.plugin.saveSettings();
});
});
} else {
colorSetting.addText((text) => {
text.setValue(this.plugin.settings.highlightColor);
text.inputEl.type = "color";
text.inputEl.addEventListener("change", async () => {
var normalized = normalizeHexColor(text.getValue());
text.setValue(normalized);
if (this.plugin.settings.highlightColor === normalized) return;
this.plugin.settings.highlightColor = normalized;
await this.plugin.saveSettings();
});
});
}
var opacitySetting = new obsidian.Setting(containerEl)
.setName("Highlight opacity")
.setDesc("Opacity of the highlight background from 0% to 100%.");
if (typeof opacitySetting.addSlider === "function") {
opacitySetting.addSlider((slider) => {
slider.setLimits(0, 100, 5);
slider.setValue(Math.round(this.plugin.settings.highlightOpacity * 100));
if (typeof slider.setDynamicTooltip === "function") slider.setDynamicTooltip();
slider.onChange(async (value) => {
var normalized = clampOpacity(value / 100);
if (this.plugin.settings.highlightOpacity === normalized) return;
this.plugin.settings.highlightOpacity = normalized;
await this.plugin.saveSettings();
});
});
} else {
opacitySetting.addText((text) => {
var commitOpacity = async () => {
var normalized = clampOpacity(parseFloat(text.getValue()) / 100);
var displayValue = String(Math.round(normalized * 100));
if (text.getValue() !== displayValue) text.setValue(displayValue);
if (this.plugin.settings.highlightOpacity === normalized) return;
this.plugin.settings.highlightOpacity = normalized;
await this.plugin.saveSettings();
};
text.setPlaceholder(String(Math.round(DEFAULT_SETTINGS.highlightOpacity * 100)));
text.setValue(String(Math.round(this.plugin.settings.highlightOpacity * 100)));
text.inputEl.type = "number";
text.inputEl.min = "0";
text.inputEl.max = "100";
text.inputEl.step = "5";
text.inputEl.addEventListener("change", commitOpacity);
text.inputEl.addEventListener("blur", commitOpacity);
});
}
}
}
module.exports = EmbeddedOmnisearchPlugin;

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"id": "embedded-omnisearch",
"name": "Embedded-Omnisearch",
"version": "1.1.0",
"minAppVersion": "1.0.0",
"description": "Inline search UI powered by Omnisearch, with configurable page size, highlight color, and highlight opacity.",
"author": "obsidian-snippets",
"isDesktopOnly": false
}

216
styles.css Normal file
View file

@ -0,0 +1,216 @@
/* === Embedded Omnisearch Plugin Styles === */
/* --- Wrapper --- */
.eo-wrap {
margin: 16px 0 10px;
width: 100%;
box-sizing: border-box;
font-family: var(--font-interface);
}
/* --- Input --- */
.eo-input-wrap {
position: relative;
}
.eo-wrap input.omnisearch-input-field {
width: 100%;
box-sizing: border-box;
padding: 10px 44px 10px 12px;
border: 0;
border-radius: 6px;
background: var(--background-secondary);
color: var(--text-normal);
font-size: 0.8rem;
outline: none;
box-shadow: inset 0 0 0 1px var(--background-modifier-border);
}
.eo-wrap input.omnisearch-input-field:focus,
.eo-wrap input.omnisearch-input-field.has-value {
box-shadow: inset 0 0 0 3px var(--background-modifier-border-hover) !important;
}
.eo-wrap input.omnisearch-input-field::selection {
background: var(--text-selection);
}
/* --- Clear button --- */
.eo-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
border-radius: 999px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
z-index: 10;
font-size: 12px;
line-height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s;
}
.eo-clear:hover {
color: var(--text-normal);
background: transparent;
box-shadow: none;
}
/* --- Status --- */
.eo-status {
margin: 6px 0 0;
font-size: 0.85em;
color: var(--text-muted);
}
/* --- Results --- */
.eo-results {
margin-top: 8px;
width: 100%;
}
/* --- Results Table --- */
.eo-results-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
margin: 0;
border: 0;
background: transparent;
}
.eo-results-table,
.eo-results-table thead,
.eo-results-table tbody,
.eo-results-table tr,
.eo-results-table th,
.eo-results-table td,
.eo-results-table colgroup,
.eo-results-table col {
border: 0 !important;
box-shadow: none !important;
background: transparent;
box-sizing: border-box;
}
.eo-col-file {
width: 24%;
}
.eo-col-score {
width: 8%;
}
.eo-col-preview {
width: 68%;
}
.eo-results-head-cell,
.eo-results-cell {
padding: 8px 10px;
vertical-align: top;
}
.eo-results-head-cell {
padding-top: 6px;
padding-bottom: 8px;
text-align: left;
font-size: 0.85em;
font-weight: 600;
color: var(--text-muted);
}
.eo-results-row:hover,
.eo-results-row:hover .eo-results-cell {
background: var(--background-secondary);
}
.eo-results-file,
.eo-results-preview {
overflow-wrap: anywhere;
}
.eo-results-link {
font-weight: 600;
text-decoration: none;
color: var(--text-normal);
}
.eo-results-link:hover {
text-decoration: underline;
color: var(--text-accent);
}
.eo-results-score,
.eo-results-head-score {
text-align: left;
color: var(--text-muted);
white-space: nowrap;
}
.eo-results-score {
font-size: 0.85em;
}
.eo-results-preview {
font-size: 0.9em;
line-height: 1.5;
color: var(--text-muted);
}
/* --- Pagination --- */
.eo-page-bar {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin: 8px 0 0;
}
.eo-page-btn {
padding: 6px 16px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-secondary);
color: var(--text-normal);
cursor: pointer;
font-size: 1em;
transition: border-color 0.15s;
}
.eo-page-btn:hover:not(:disabled) {
border-color: var(--background-modifier-border-hover);
}
.eo-page-info {
font-size: 0.85em;
color: var(--text-muted);
min-width: 80px;
text-align: center;
}
/* --- Popover --- */
.eo-popover {
position: fixed;
padding: 4px 10px;
background: var(--background-secondary);
color: var(--text-muted);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
font-size: 0.8em;
pointer-events: none;
z-index: 9999;
max-width: 500px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}