From 94d987466fb390576fcfc086ed2aec3658ac18ee Mon Sep 17 00:00:00 2001 From: fnsign Date: Sat, 9 May 2026 14:53:31 +0200 Subject: [PATCH] first commit --- README.md | 149 ++++++++++++ main.js | 626 ++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 9 + styles.css | 216 +++++++++++++++++ 4 files changed, 1000 insertions(+) create mode 100644 README.md create mode 100644 main.js create mode 100644 manifest.json create mode 100644 styles.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..2604d85 --- /dev/null +++ b/README.md @@ -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: + + ``` + /.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 diff --git a/main.js b/main.js new file mode 100644 index 0000000..b68b4b0 --- /dev/null +++ b/main.js @@ -0,0 +1,626 @@ +'use strict'; + +var obsidian = require('obsidian'); + +/* === Pre-compiled patterns === */ +var RE_HTML = /[&<>"']/g; +var HTML_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; +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 += '' + + escHtml(o.slice(r[0], r[1])) + ''; + 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; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3a57713 --- /dev/null +++ b/manifest.json @@ -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 +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..647c1ea --- /dev/null +++ b/styles.css @@ -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; +}