mirror of
https://github.com/westerngua/obsidian-zotero-citations.git
synced 2026-07-22 06:51:52 +00:00
0.2.1
This commit is contained in:
parent
8b7d1cb58d
commit
d02cfe32ee
19 changed files with 609 additions and 14413 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -6,6 +6,22 @@
|
|||
|
||||
---
|
||||
|
||||
## [0.2.1] - 2026-05-03
|
||||
|
||||
### 改进
|
||||
|
||||
- 对齐 Obsidian 官方插件规范:命令名、README、发布说明与实际行为同步,不再依赖手工命令前缀
|
||||
- 将静态内联样式迁移到 `styles.css`,并把状态颜色改为 Obsidian 主题变量
|
||||
- 构建产物改为生产模式压缩,GitHub Release 说明同步要求附带 `styles.css`
|
||||
|
||||
### 修复
|
||||
|
||||
- 扩充 README 披露说明,补充本地回环通信、临时文件、`pandoc` / `sqlite3` / `osascript` 等外部访问说明
|
||||
- 改用 Obsidian `Platform` / `FileSystemAdapter` API,减少与官方规范不符的运行时用法
|
||||
- 仓库不再跟踪 `main.js`,源码仓库与本地运行目录职责更清晰
|
||||
|
||||
---
|
||||
|
||||
## [0.2.0] - 2026-05-03
|
||||
|
||||
### 新增
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -34,6 +34,7 @@
|
|||
1. 从 GitHub Releases 页面下载以下文件:
|
||||
- `main.js` — 插件主程序
|
||||
- `manifest.json` — 插件清单
|
||||
- `styles.css` — 插件样式
|
||||
2. 将以上文件放入 vault 的 `.obsidian/plugins/zotero-citations/` 目录(如目录不存在则手动创建)
|
||||
3. 在 Obsidian 设置 → 第三方插件中启用 **Zotero Citations**
|
||||
4. 确保 Zotero 已打开且 Better BibTeX 已安装
|
||||
|
|
@ -47,11 +48,21 @@
|
|||
|
||||
---
|
||||
|
||||
## 披露说明
|
||||
|
||||
- **网络访问**:插件会通过 `127.0.0.1` 本地回环地址与 Zotero / Better BibTeX 通信,不会连接插件自建服务器。
|
||||
- **外部文件与可执行程序访问**:插件会读取本机 Zotero 样式目录;在部分回退场景下可能复制并读取本机 Zotero 数据库到系统临时目录;导出 Word 时会调用本地 `pandoc`;数据库回退解析时可能调用本地 `sqlite3`;在 macOS 上,为了从 Zotero 选择器返回 Obsidian,插件可能调用系统 `osascript`。
|
||||
- **本地数据存储**:插件会在 Obsidian 的插件数据中保存设置项和引用缓存。
|
||||
- **账户 / 付费 / 广告 / 遥测**:插件不要求登录账户,不含广告,不含内购,也不主动收集遥测数据。
|
||||
- **源码状态**:插件源码已公开在 GitHub 仓库中,并以 MIT 许可证发布:<https://github.com/WesternGua/obsidian-zotero-citations>
|
||||
|
||||
---
|
||||
|
||||
## 快速上手
|
||||
|
||||
### 1. 插入引用
|
||||
|
||||
在命令面板中执行 `Zotero Citations: 插入引用`,或点击标题栏中的插入引用图标。
|
||||
在命令面板中搜索“插入引用”,或点击标题栏中的插入引用图标。
|
||||
|
||||
插件会优先调用 Zotero 原生引文选择器——在弹出窗口中搜索并选择条目,可填写页码等定位符,点击右上角 ✔ 确认插入。
|
||||
|
||||
|
|
@ -59,7 +70,7 @@
|
|||
|
||||
> **注意**:
|
||||
> - 插件会在脚注正文开头写入 `<!-- zotero:ITEMKEY:locator -->` 形式的隐藏元数据,请勿手动删除,否则插件无法识别该引用。
|
||||
> - 定稿后如需移除插件元数据,可执行 `Zotero Citations: 解除引用链接`(不可逆),插件会删除隐藏元数据并保留可见引用文本。
|
||||
> - 定稿后如需移除插件元数据,可执行“解除引用链接”(不可逆),插件会删除隐藏元数据并保留可见引用文本。
|
||||
|
||||

|
||||
|
||||
|
|
@ -71,20 +82,20 @@
|
|||
|
||||
### 3. 切换引用样式
|
||||
|
||||
执行 `Zotero Citations: 文档首选项` 打开文档首选项窗口。插件会动态读取 Zotero 中已安装的所有 CSL 样式,以可搜索列表形式展示;选择所需样式后,可同时切换脚注/尾注模式,并一键应用到当前文档的所有引用:
|
||||
执行“文档首选项”打开文档首选项窗口。插件会动态读取 Zotero 中已安装的所有 CSL 样式,以可搜索列表形式展示;选择所需样式后,可同时切换脚注/尾注模式,并一键应用到当前文档的所有引用:
|
||||
|
||||

|
||||
|
||||
### 4. 插入参考书目
|
||||
|
||||
执行 `Zotero Citations: 插入参考书目`,插件会在光标位置生成当前文档所有引用的参考文献列表。参考书目在导出 Word 时也会保留。
|
||||
执行“插入参考书目”,插件会在光标位置生成当前文档所有引用的参考文献列表。参考书目在导出 Word 时也会保留。
|
||||
|
||||

|
||||
|
||||
### 5. 导出为 Word
|
||||
|
||||
1. 在命令面板中执行 `Zotero Citations: 检测 Pandoc 是否可用`,确认 Pandoc 正常工作
|
||||
2. 执行 `Zotero Citations: 导出为 Word (.docx)`
|
||||
1. 在命令面板中执行“检测 Pandoc 是否可用”,确认 Pandoc 正常工作
|
||||
2. 执行“导出为 Word (.docx)”
|
||||
|
||||
导出的 Word 文档包含正确格式的脚注,正文为宋体小四、1.5 倍行距、两端对齐、首行缩进两字符,标题为黑体。
|
||||
|
||||
|
|
@ -118,7 +129,7 @@
|
|||
|
||||
## 命令列表
|
||||
|
||||
所有命令均以 `Zotero Citations:` 为前缀,方便在命令面板中搜索:
|
||||
以下为插件提供的命令;在命令面板中可直接按命令名称搜索:
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
|
|
|
|||
25
README_EN.md
25
README_EN.md
|
|
@ -32,6 +32,7 @@
|
|||
1. Download the following files from the GitHub Releases page:
|
||||
- `main.js` — plugin runtime
|
||||
- `manifest.json` — plugin manifest
|
||||
- `styles.css` — plugin styles
|
||||
2. Place the files into your vault's `.obsidian/plugins/zotero-citations/` directory (create it if it does not exist)
|
||||
3. Enable **Zotero Citations** in Obsidian Settings → Community plugins
|
||||
4. Make sure Zotero is running and Better BibTeX is installed
|
||||
|
|
@ -45,11 +46,21 @@ This version has been developed and tested primarily on **macOS**. Linux and Win
|
|||
|
||||
---
|
||||
|
||||
## Disclosures
|
||||
|
||||
- **Network use**: the plugin talks to Zotero / Better BibTeX over the local loopback address `127.0.0.1`; it does not rely on a plugin-operated remote server.
|
||||
- **External files and executables**: the plugin reads the local Zotero styles directory; in fallback scenarios it may copy and read the local Zotero database in the system temp directory; for Word export it invokes the local `pandoc` executable; for database fallback parsing it may invoke the local `sqlite3` executable; on macOS it may invoke the system `osascript` command to return focus to Obsidian after the Zotero picker closes.
|
||||
- **Local data storage**: the plugin stores settings and citation cache data in Obsidian's plugin data storage.
|
||||
- **Accounts / payments / ads / telemetry**: the plugin does not require an account, does not include ads, does not include in-app payments, and does not intentionally collect telemetry.
|
||||
- **Source availability**: the plugin source code is published on GitHub under the MIT license: <https://github.com/WesternGua/obsidian-zotero-citations>
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Insert a Citation
|
||||
|
||||
Run `Zotero Citations: Insert citation` from the command palette, or click the citation icon in the title bar.
|
||||
Search for `Insert citation` in the command palette, or click the citation icon in the title bar.
|
||||
|
||||
The plugin will first try to open Zotero's native citation picker — search for items, add a page number or other locator, and confirm with the checkmark button.
|
||||
|
||||
|
|
@ -57,7 +68,7 @@ Inserted citations appear as footnotes or endnotes depending on your current cit
|
|||
|
||||
> **Note**:
|
||||
> - The plugin writes hidden metadata (`<!-- zotero:ITEMKEY:locator -->`) at the beginning of each note. Do not remove it manually, or the plugin will not be able to track the citation.
|
||||
> - When you are ready to finalize, run `Zotero Citations: Unlink citations` (irreversible) to strip the hidden metadata while keeping the visible citation text.
|
||||
> - When you are ready to finalize, run `Unlink citations` (irreversible) to strip the hidden metadata while keeping the visible citation text.
|
||||
|
||||

|
||||
|
||||
|
|
@ -69,20 +80,20 @@ With Word-style footnote display enabled, hover over a superscript number to pre
|
|||
|
||||
### 3. Switch Citation Style
|
||||
|
||||
Run `Zotero Citations: Document preferences` to open the preferences panel. The plugin dynamically reads all CSL styles installed in your Zotero and presents them in a searchable list. Pick a style, optionally switch between footnote/endnote mode, and apply the change to all citations in the current document at once:
|
||||
Run `Document preferences` to open the preferences panel. The plugin dynamically reads all CSL styles installed in your Zotero and presents them in a searchable list. Pick a style, optionally switch between footnote/endnote mode, and apply the change to all citations in the current document at once:
|
||||
|
||||

|
||||
|
||||
### 4. Insert a Bibliography
|
||||
|
||||
Run `Zotero Citations: Insert bibliography` to generate a formatted reference list at the cursor position. The bibliography is also preserved when exporting to Word.
|
||||
Run `Insert bibliography` to generate a formatted reference list at the cursor position. The bibliography is also preserved when exporting to Word.
|
||||
|
||||

|
||||
|
||||
### 5. Export to Word
|
||||
|
||||
1. Run `Zotero Citations: Check whether Pandoc is available` first to confirm Pandoc is working.
|
||||
2. Run `Zotero Citations: Export to Word (.docx)`.
|
||||
1. Run `Check whether Pandoc is available` first to confirm Pandoc is working.
|
||||
2. Run `Export to Word (.docx)`.
|
||||
|
||||
The exported Word document contains properly formatted footnotes, with body text in SimSun 12pt, 1.5 line spacing, justified alignment, first-line indent, and headings in SimHei.
|
||||
|
||||
|
|
@ -114,7 +125,7 @@ The exported Word document contains properly formatted footnotes, with body text
|
|||
|
||||
## Command List
|
||||
|
||||
All commands are prefixed with `Zotero Citations:` for easy discovery in the command palette:
|
||||
The plugin provides the following commands; you can search for them directly by name in the command palette:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# Zotero Citations v0.2.0
|
||||
# Zotero Citations v0.2.1
|
||||
|
||||
## 新功能
|
||||
## 官方规范对齐 / Compliance
|
||||
|
||||
- **动态 CSL 样式读取** — 文档首选项窗口自动扫描 Zotero 已安装的所有 CSL 样式,支持搜索过滤和刷新
|
||||
- **标题栏按钮独立控制** — 6 个快捷图标可在设置中单独开关,按需定制工作区
|
||||
- **参考书目格式优化** — 自动添加一级标题 + 引用块,导出 Word 时无缩进、两端对齐
|
||||
- **命令与文档同步** — 命令面板名称、README 与发布说明不再保留错误的手工前缀说法
|
||||
- **样式合规化** — 静态内联样式迁移到 `styles.css`,状态色改为 Obsidian 主题变量
|
||||
- **构建与发布同步** — 生产构建改为压缩输出,Release 资产说明补充 `styles.css`
|
||||
|
||||
## 改进
|
||||
## 披露与仓库整理
|
||||
|
||||
- 命令面板统一 `Zotero Citations:` 前缀,不再与其他插件命令混淆
|
||||
- 文档首选项窗口完全重写,样式列表可搜索、可刷新
|
||||
- Modal 对话框标题对齐优化
|
||||
- Word 导出模板样式调整(参考书目段落取消缩进)
|
||||
- **README 披露补全** — 补充回环网络、临时文件、本地 `pandoc` / `sqlite3` / `osascript` 调用说明
|
||||
- **仓库结构更标准** — `main.js` 继续作为本地运行产物存在,但不再纳入 Git 跟踪
|
||||
- **开发文档同步** — `docs/DEVELOPMENT.md` 与当前目录结构、Release 流程保持一致
|
||||
|
||||
## 修复
|
||||
## 代码修复
|
||||
|
||||
- `toolbarButtons` 设置首次加载兼容性修复
|
||||
- 改用 Obsidian `Platform` / `FileSystemAdapter` API,减少与官方规范不符的实现
|
||||
- 清理剩余静态内联样式,设置页与各模态窗口样式统一交给 `styles.css`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ This plugin directory now follows a more standard Obsidian plugin source-repo la
|
|||
|
||||
```text
|
||||
zotero-citations/
|
||||
├── .github/workflows/ # GitHub Actions
|
||||
├── assets/screenshots/ # README images
|
||||
├── docs/ # Project docs
|
||||
├── src/ # TypeScript source
|
||||
|
|
@ -14,14 +13,16 @@ zotero-citations/
|
|||
├── LICENSE
|
||||
├── README.md
|
||||
├── README_EN.md
|
||||
├── RELEASE_NOTES.md
|
||||
├── esbuild.config.mjs
|
||||
├── manifest.json
|
||||
├── package.json
|
||||
├── package-lock.json
|
||||
├── styles.css # Runtime stylesheet loaded by Obsidian
|
||||
├── tsconfig.json
|
||||
├── version-bump.mjs
|
||||
├── versions.json
|
||||
└── main.js # Build output used by Obsidian at runtime
|
||||
└── main.js # Generated build output used locally by Obsidian (kept untracked in Git)
|
||||
```
|
||||
|
||||
## Local Workflow
|
||||
|
|
@ -45,9 +46,9 @@ This folder is both:
|
|||
1. a source repository layout, and
|
||||
2. the live plugin directory under `.obsidian/plugins/`
|
||||
|
||||
So `main.js` must continue to exist locally for Obsidian to load the plugin, even though `.gitignore` excludes it for a cleaner source-repo workflow.
|
||||
So `main.js` must continue to exist locally for Obsidian to load the plugin, even though `.gitignore` excludes it from the GitHub source repository.
|
||||
|
||||
Likewise, `data.json` is local runtime state and should stay untracked.
|
||||
Likewise, `data.json` is local runtime state and should stay untracked. In contrast, `styles.css` is a real plugin asset and should stay versioned.
|
||||
|
||||
## Release Metadata
|
||||
|
||||
|
|
@ -62,4 +63,4 @@ Likewise, `data.json` is local runtime state and should stay untracked.
|
|||
3. Update `CHANGELOG.md`
|
||||
4. Bump the version with `npm version ...`
|
||||
5. Confirm `manifest.json` and `versions.json` were updated
|
||||
6. Attach `main.js` and `manifest.json` to the GitHub release if distributing release artifacts
|
||||
6. Attach `main.js`, `manifest.json`, and `styles.css` to the GitHub release if distributing release artifacts
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ esbuild.build({
|
|||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
minify: prod,
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
platform: "node",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "zotero-citations",
|
||||
"name": "Zotero Citations",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "Zotero citation management with Word export via Pandoc.",
|
||||
"author": "WesternGua",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zotero-citations",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Zotero citation management with Word export via Pandoc.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* ExportManager.ts – Pandoc export and reference document generation
|
||||
*/
|
||||
import { Notice } from "obsidian";
|
||||
import { Notice, Platform } from "obsidian";
|
||||
import { t } from "./i18n";
|
||||
import { ZoteroCitationsSettings, DEFAULT_SETTINGS } from "./settings";
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ export class ExportManager {
|
|||
|
||||
static q(s: string): string {
|
||||
if (!s) return "";
|
||||
return process.platform === "win32"
|
||||
return Platform.isWin
|
||||
? `"${s.replace(/"/g, '\\"')}"`
|
||||
: `'${s.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* ZoteroAPI.ts – Communication with Zotero via Better BibTeX HTTP API
|
||||
* Improvement 2: Added getInstalledStyles() to dynamically read CSL styles from Zotero
|
||||
*/
|
||||
import { requestUrl } from "obsidian";
|
||||
import { Platform, requestUrl } from "obsidian";
|
||||
import * as nodeHttp from "http";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
|
@ -102,7 +102,7 @@ export class ZoteroAPI {
|
|||
|
||||
async ping(): Promise<boolean> {
|
||||
try {
|
||||
const r = await requestUrl({ url: `${this.baseUrl}/connector/ping`, method: "GET", throw: false } as any);
|
||||
const r = await requestUrl({ url: `${this.baseUrl}/connector/ping`, method: "GET", throw: false });
|
||||
return r.status === 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
|
@ -120,10 +120,10 @@ export class ZoteroAPI {
|
|||
const home = os.homedir();
|
||||
const candidates: string[] = [];
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
if (Platform.isMacOS) {
|
||||
candidates.push(path.join(home, "Zotero", "styles"));
|
||||
candidates.push(path.join(home, "Library", "Application Support", "Zotero", "Profiles"));
|
||||
} else if (process.platform === "win32") {
|
||||
} else if (Platform.isWin) {
|
||||
const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
||||
candidates.push(path.join(appdata, "Zotero", "Zotero", "Profiles"));
|
||||
candidates.push(path.join(home, "Zotero", "styles"));
|
||||
|
|
@ -260,7 +260,7 @@ export class ZoteroAPI {
|
|||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", method: "item.search", params: [query], id: 1 }),
|
||||
throw: false,
|
||||
} as any);
|
||||
});
|
||||
if (r.status !== 200) throw new Error(`HTTP ${r.status}`);
|
||||
const d = r.json;
|
||||
if (d.error) throw new Error(d.error.message);
|
||||
|
|
@ -280,7 +280,7 @@ export class ZoteroAPI {
|
|||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", method: "item.citationkey", params: [itemKeys], id: 2 }),
|
||||
throw: false,
|
||||
} as any);
|
||||
});
|
||||
if (r.status !== 200) return map;
|
||||
const d = r.json;
|
||||
if (d.error || !d.result || typeof d.result !== "object") return map;
|
||||
|
|
@ -308,7 +308,7 @@ export class ZoteroAPI {
|
|||
id: 3,
|
||||
}),
|
||||
throw: false,
|
||||
} as any);
|
||||
});
|
||||
if (r.status === 200) {
|
||||
const d = r.json;
|
||||
if (!d?.error && d?.result) {
|
||||
|
|
@ -346,7 +346,7 @@ export class ZoteroAPI {
|
|||
id: 4,
|
||||
}),
|
||||
throw: false,
|
||||
} as any);
|
||||
});
|
||||
const d = r.json;
|
||||
if (r.status === 200 && !d.error && d.result) {
|
||||
const items = JSON.parse(d.result);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
/**
|
||||
* FootnoteExtension.ts – CodeMirror extension for Word-style footnote rendering
|
||||
*/
|
||||
import { Component, MarkdownRenderer, MarkdownView, Notice } from "obsidian";
|
||||
import { App, Component, MarkdownRenderer, MarkdownView, Notice } from "obsidian";
|
||||
import { ViewPlugin, WidgetType, Decoration, EditorView, ViewUpdate } from "@codemirror/view";
|
||||
import { appT } from "../i18n";
|
||||
import { CitationManager } from "../CitationManager";
|
||||
import { ZoteroItem } from "../ZoteroAPI";
|
||||
|
||||
export interface FootnoteExtensionOptions {
|
||||
isEnabled: () => boolean;
|
||||
app: any;
|
||||
app: App;
|
||||
getSourcePath: () => string;
|
||||
}
|
||||
|
||||
|
|
@ -18,22 +19,43 @@ interface PreviewInfo {
|
|||
edit?: EditInfo | null;
|
||||
}
|
||||
|
||||
interface EditInfo {
|
||||
interface PopoverSpec {
|
||||
app: App;
|
||||
getSourcePath: () => string;
|
||||
markdown: string;
|
||||
fallbackText: string;
|
||||
edit?: EditInfo;
|
||||
}
|
||||
|
||||
type PopoverAttacher = (target: HTMLElement, spec: PopoverSpec) => void;
|
||||
|
||||
interface InlineEditInfo {
|
||||
kind: "inline" | "endnote";
|
||||
key: string;
|
||||
locator: string;
|
||||
label?: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
interface EndnoteEditInfo {
|
||||
kind: "endnote";
|
||||
key: string;
|
||||
locator: string;
|
||||
label: string;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
type EditInfo = Omit<InlineEditInfo, "kind"> & { kind: "inline" } | EndnoteEditInfo;
|
||||
|
||||
// ── Widget ─────────────────────────────────────────────────────────────────
|
||||
class FnWidget extends WidgetType {
|
||||
constructor(
|
||||
public num: number,
|
||||
public preview: PreviewInfo,
|
||||
public app: any,
|
||||
public app: App,
|
||||
public getSourcePath: () => string,
|
||||
public attachPopover: PopoverAttacher,
|
||||
public identifier: string,
|
||||
public domId: string,
|
||||
public isHighlighted: boolean = false,
|
||||
|
|
@ -75,7 +97,7 @@ class FnWidget extends WidgetType {
|
|||
marker.addEventListener("mousedown", (event) => event.preventDefault());
|
||||
marker.addEventListener("click", (event) => event.preventDefault());
|
||||
|
||||
attachRenderedPopover(marker, {
|
||||
this.attachPopover(marker, {
|
||||
app: this.app,
|
||||
getSourcePath: this.getSourcePath,
|
||||
markdown: this.preview.markdown,
|
||||
|
|
@ -103,15 +125,16 @@ class FnWidget extends WidgetType {
|
|||
|
||||
// ── Extension factory ──────────────────────────────────────────────────────
|
||||
export function createFootnoteExtension(options: FootnoteExtensionOptions) {
|
||||
const attachPopover = createPopoverAttacher();
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: any;
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = buildDeco(view, options);
|
||||
this.decorations = buildDeco(view, options, attachPopover);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = buildDeco(update.view, options);
|
||||
this.decorations = buildDeco(update.view, options, attachPopover);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -119,7 +142,7 @@ export function createFootnoteExtension(options: FootnoteExtensionOptions) {
|
|||
);
|
||||
}
|
||||
|
||||
function buildDeco(view: EditorView, options: FootnoteExtensionOptions) {
|
||||
function buildDeco(view: EditorView, options: FootnoteExtensionOptions, attachPopover: PopoverAttacher) {
|
||||
if (!options.isEnabled()) return Decoration.none;
|
||||
|
||||
const doc = view.state.doc.toString();
|
||||
|
|
@ -194,7 +217,7 @@ function buildDeco(view: EditorView, options: FootnoteExtensionOptions) {
|
|||
Decoration.replace({
|
||||
widget: new FnWidget(
|
||||
num, { markdown, text, edit }, options.app,
|
||||
options.getSourcePath, identifier, domId,
|
||||
options.getSourcePath, attachPopover, identifier, domId,
|
||||
isInsideHighlight(doc, start, end),
|
||||
),
|
||||
}).range(start, end),
|
||||
|
|
@ -232,7 +255,7 @@ function countHighlightDelimiters(text: string): number {
|
|||
return count;
|
||||
}
|
||||
|
||||
function extractInlinePreview(rawMarker: string, num: number, app: any): { markdown: string; text: string; edit?: EditInfo | null } {
|
||||
function extractInlinePreview(rawMarker: string, num: number, app: App): { markdown: string; text: string; edit?: EditInfo | null } {
|
||||
const body = rawMarker.slice(2, -1);
|
||||
const metadata = parseZoteroMetadata(body);
|
||||
const markdown = metadata.markdown.trim();
|
||||
|
|
@ -244,7 +267,7 @@ function extractInlinePreview(rawMarker: string, num: number, app: any): { markd
|
|||
};
|
||||
}
|
||||
|
||||
function buildEndnotePreviewMap(doc: string, app: any): Map<string, { markdown: string; text: string; edit?: EditInfo | null }> {
|
||||
function buildEndnotePreviewMap(doc: string, app: App): Map<string, { markdown: string; text: string; edit?: EditInfo | null }> {
|
||||
const map = new Map<string, { markdown: string; text: string; edit?: EditInfo | null }>();
|
||||
const lines = doc.split("\n");
|
||||
const lineOffsets: number[] = [];
|
||||
|
|
@ -301,80 +324,125 @@ function normalizeTooltipText(text: string): string {
|
|||
}
|
||||
|
||||
// ── Popover system ─────────────────────────────────────────────────────────
|
||||
let activePopover: {
|
||||
interface ActivePopover {
|
||||
target: HTMLElement; popover: HTMLElement; component: Component;
|
||||
hideTimer: number | null; reposition: () => void;
|
||||
onPopoverEnter: () => void; onPopoverLeave: () => void;
|
||||
} | null = null;
|
||||
|
||||
interface PopoverSpec {
|
||||
app: any;
|
||||
getSourcePath: () => string;
|
||||
markdown: string;
|
||||
fallbackText: string;
|
||||
edit?: EditInfo;
|
||||
}
|
||||
|
||||
function attachRenderedPopover(target: HTMLElement, spec: PopoverSpec): void {
|
||||
const show = () => showRenderedPopover(target, spec);
|
||||
const scheduleHide = () => schedulePopoverHide(target);
|
||||
target.addEventListener("mouseenter", show);
|
||||
target.addEventListener("mouseleave", scheduleHide);
|
||||
target.addEventListener("mousemove", show);
|
||||
target.addEventListener("pointerenter", show);
|
||||
target.addEventListener("pointerleave", scheduleHide);
|
||||
target.addEventListener("pointermove", show);
|
||||
target.addEventListener("focus", show);
|
||||
target.addEventListener("blur", scheduleHide);
|
||||
interface ZoteroPluginLike {
|
||||
settings: { cslStyle: string };
|
||||
getCached: (key: string) => ZoteroItem | undefined;
|
||||
fetchAndCache: (key: string) => Promise<ZoteroItem | null>;
|
||||
}
|
||||
|
||||
function showRenderedPopover(target: HTMLElement, spec: PopoverSpec): void {
|
||||
if (!spec.markdown && !spec.fallbackText) return;
|
||||
if (activePopover?.target === target) {
|
||||
type AppWithPlugins = App & {
|
||||
plugins?: {
|
||||
plugins?: Record<string, ZoteroPluginLike | undefined>;
|
||||
};
|
||||
};
|
||||
|
||||
function createPopoverAttacher(): PopoverAttacher {
|
||||
let activePopover: ActivePopover | null = null;
|
||||
|
||||
const cancelPopoverHide = (): void => {
|
||||
if (!activePopover?.hideTimer) return;
|
||||
window.clearTimeout(activePopover.hideTimer);
|
||||
activePopover.hideTimer = null;
|
||||
};
|
||||
|
||||
const destroyActivePopover = (): void => {
|
||||
if (!activePopover) return;
|
||||
const { popover, component, reposition, onPopoverEnter, onPopoverLeave, hideTimer } = activePopover;
|
||||
if (hideTimer) window.clearTimeout(hideTimer);
|
||||
popover.removeEventListener("mouseenter", onPopoverEnter);
|
||||
popover.removeEventListener("mouseleave", onPopoverLeave);
|
||||
window.removeEventListener("scroll", reposition, true);
|
||||
window.removeEventListener("resize", reposition);
|
||||
component.unload();
|
||||
popover.remove();
|
||||
activePopover = null;
|
||||
};
|
||||
|
||||
const schedulePopoverHide = (target: HTMLElement): void => {
|
||||
if (!activePopover || activePopover.target !== target) return;
|
||||
cancelPopoverHide();
|
||||
activePopover.reposition();
|
||||
return;
|
||||
}
|
||||
destroyActivePopover();
|
||||
activePopover.hideTimer = window.setTimeout(() => {
|
||||
if (activePopover?.target === target) destroyActivePopover();
|
||||
}, 80);
|
||||
};
|
||||
|
||||
const popover = document.createElement("div");
|
||||
popover.className = "popover hover-popover zotero-footnote-popover";
|
||||
popover.style.position = "fixed";
|
||||
const showRenderedPopover = (target: HTMLElement, spec: PopoverSpec): void => {
|
||||
if (!spec.markdown && !spec.fallbackText) return;
|
||||
if (activePopover?.target === target) {
|
||||
cancelPopoverHide();
|
||||
activePopover.reposition();
|
||||
return;
|
||||
}
|
||||
destroyActivePopover();
|
||||
|
||||
const embed = popover.createDiv({ cls: "markdown-embed", attr: { "data-type": "footnote" } });
|
||||
const embedContent = embed.createDiv({ cls: "markdown-embed-content" });
|
||||
const preview = embedContent.createDiv({ cls: "markdown-preview-view markdown-rendered" });
|
||||
preview.setText(spec.fallbackText);
|
||||
const popover = document.createElement("div");
|
||||
popover.className = "popover hover-popover zotero-footnote-popover";
|
||||
|
||||
if (spec.edit) {
|
||||
mountLocatorEditor(embedContent, spec, target);
|
||||
}
|
||||
if (!spec.markdown.trim()) embed.addClass("mod-empty");
|
||||
const embed = popover.createDiv({ cls: "markdown-embed", attr: { "data-type": "footnote" } });
|
||||
const embedContent = embed.createDiv({ cls: "markdown-embed-content" });
|
||||
const preview = embedContent.createDiv({ cls: "markdown-preview-view markdown-rendered" });
|
||||
preview.setText(spec.fallbackText);
|
||||
|
||||
document.body.appendChild(popover);
|
||||
const component = new Component();
|
||||
const reposition = () => positionPopover(target, popover);
|
||||
const onPopoverEnter = () => cancelPopoverHide();
|
||||
const onPopoverLeave = () => schedulePopoverHide(target);
|
||||
popover.addEventListener("mouseenter", onPopoverEnter);
|
||||
popover.addEventListener("mouseleave", onPopoverLeave);
|
||||
window.addEventListener("scroll", reposition, true);
|
||||
window.addEventListener("resize", reposition);
|
||||
if (spec.edit) {
|
||||
mountLocatorEditor(
|
||||
embedContent,
|
||||
spec,
|
||||
() => activePopover?.target === target,
|
||||
destroyActivePopover,
|
||||
);
|
||||
}
|
||||
if (!spec.markdown.trim()) embed.addClass("mod-empty");
|
||||
|
||||
activePopover = { target, popover, component, hideTimer: null, reposition, onPopoverEnter, onPopoverLeave };
|
||||
reposition();
|
||||
requestAnimationFrame(reposition);
|
||||
document.body.appendChild(popover);
|
||||
const component = new Component();
|
||||
const reposition = () => positionPopover(target, popover);
|
||||
const onPopoverEnter = () => cancelPopoverHide();
|
||||
const onPopoverLeave = () => schedulePopoverHide(target);
|
||||
popover.addEventListener("mouseenter", onPopoverEnter);
|
||||
popover.addEventListener("mouseleave", onPopoverLeave);
|
||||
window.addEventListener("scroll", reposition, true);
|
||||
window.addEventListener("resize", reposition);
|
||||
|
||||
if (spec.markdown.trim()) {
|
||||
void renderPopoverMarkdown(preview, spec, component, target, reposition);
|
||||
}
|
||||
activePopover = { target, popover, component, hideTimer: null, reposition, onPopoverEnter, onPopoverLeave };
|
||||
reposition();
|
||||
requestAnimationFrame(reposition);
|
||||
|
||||
if (spec.markdown.trim()) {
|
||||
void renderPopoverMarkdown(preview, spec, component, reposition, () => activePopover?.target === target);
|
||||
}
|
||||
};
|
||||
|
||||
return (target: HTMLElement, spec: PopoverSpec): void => {
|
||||
const show = () => showRenderedPopover(target, spec);
|
||||
const scheduleHide = () => schedulePopoverHide(target);
|
||||
target.addEventListener("mouseenter", show);
|
||||
target.addEventListener("mouseleave", scheduleHide);
|
||||
target.addEventListener("mousemove", show);
|
||||
target.addEventListener("pointerenter", show);
|
||||
target.addEventListener("pointerleave", scheduleHide);
|
||||
target.addEventListener("pointermove", show);
|
||||
target.addEventListener("focus", show);
|
||||
target.addEventListener("blur", scheduleHide);
|
||||
};
|
||||
}
|
||||
|
||||
async function renderPopoverMarkdown(preview: HTMLElement, spec: PopoverSpec, component: Component, target: HTMLElement, reposition: () => void): Promise<void> {
|
||||
async function renderPopoverMarkdown(
|
||||
preview: HTMLElement,
|
||||
spec: PopoverSpec,
|
||||
component: Component,
|
||||
reposition: () => void,
|
||||
isActiveTarget: () => boolean,
|
||||
): Promise<void> {
|
||||
try {
|
||||
preview.empty();
|
||||
await MarkdownRenderer.render(spec.app, spec.markdown, preview, spec.getSourcePath(), component);
|
||||
if (activePopover?.target !== target) return;
|
||||
if (!isActiveTarget()) return;
|
||||
reposition();
|
||||
requestAnimationFrame(reposition);
|
||||
} catch (e) {
|
||||
|
|
@ -382,7 +450,12 @@ async function renderPopoverMarkdown(preview: HTMLElement, spec: PopoverSpec, co
|
|||
}
|
||||
}
|
||||
|
||||
function mountLocatorEditor(container: HTMLElement, spec: PopoverSpec, target: HTMLElement): void {
|
||||
function mountLocatorEditor(
|
||||
container: HTMLElement,
|
||||
spec: PopoverSpec,
|
||||
isActiveTarget: () => boolean,
|
||||
destroyPopover: () => void,
|
||||
): void {
|
||||
const wrap = container.createDiv({ cls: "zotero-footnote-locator-editor" });
|
||||
const input = wrap.createEl("input", { type: "text" });
|
||||
input.value = spec.edit!.locator || "";
|
||||
|
|
@ -394,33 +467,38 @@ function mountLocatorEditor(container: HTMLElement, spec: PopoverSpec, target: H
|
|||
saveBtn.disabled = true;
|
||||
const ok = await applyLocatorEdit(spec, input.value.trim());
|
||||
saveBtn.disabled = false;
|
||||
if (ok && activePopover?.target === target) destroyActivePopover();
|
||||
if (ok && isActiveTarget()) destroyPopover();
|
||||
};
|
||||
saveBtn.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); void save(); });
|
||||
input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); void save(); } });
|
||||
}
|
||||
|
||||
async function applyLocatorEdit(spec: PopoverSpec, locator: string): Promise<boolean> {
|
||||
const plugin = spec.app.plugins.plugins["zotero-citations"];
|
||||
const edit = spec.edit;
|
||||
if (!edit) {
|
||||
new Notice(appT(spec.app, "footnote.noEditor"));
|
||||
return false;
|
||||
}
|
||||
const plugin = (spec.app as AppWithPlugins).plugins?.plugins?.["zotero-citations"];
|
||||
const view = spec.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const editor = view?.editor;
|
||||
if (!plugin || !editor) {
|
||||
new Notice(appT(spec.app, "footnote.noEditor"));
|
||||
return false;
|
||||
}
|
||||
const item = plugin.getCached(spec.edit!.key) || await plugin.fetchAndCache(spec.edit!.key);
|
||||
const item = plugin.getCached(edit.key) || await plugin.fetchAndCache(edit.key);
|
||||
if (!item) {
|
||||
new Notice(appT(spec.app, "footnote.noItem"));
|
||||
return false;
|
||||
}
|
||||
const page = locator || undefined;
|
||||
const style = plugin.settings.cslStyle;
|
||||
if (spec.edit!.kind === "inline") {
|
||||
if (edit.kind === "inline") {
|
||||
const replacement = CitationManager.buildInlineFootnote(item, style, page);
|
||||
editor.replaceRange(replacement, editor.offsetToPos(spec.edit!.from), editor.offsetToPos(spec.edit!.to));
|
||||
editor.replaceRange(replacement, editor.offsetToPos(edit.from), editor.offsetToPos(edit.to));
|
||||
} else {
|
||||
const replacement = CitationManager.buildEndnoteDef((spec.edit as any).label, item, style, page);
|
||||
editor.replaceRange(replacement, editor.offsetToPos(spec.edit!.from), editor.offsetToPos(spec.edit!.to));
|
||||
const replacement = CitationManager.buildEndnoteDef(edit.label, item, style, page);
|
||||
editor.replaceRange(replacement, editor.offsetToPos(edit.from), editor.offsetToPos(edit.to));
|
||||
}
|
||||
new Notice(appT(spec.app, "footnote.updated"));
|
||||
return true;
|
||||
|
|
@ -440,30 +518,3 @@ function positionPopover(target: HTMLElement, popover: HTMLElement): void {
|
|||
popover.style.top = `${top}px`;
|
||||
popover.style.left = `${left}px`;
|
||||
}
|
||||
|
||||
function schedulePopoverHide(target: HTMLElement): void {
|
||||
if (!activePopover || activePopover.target !== target) return;
|
||||
cancelPopoverHide();
|
||||
activePopover.hideTimer = window.setTimeout(() => {
|
||||
if (activePopover?.target === target) destroyActivePopover();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function cancelPopoverHide(): void {
|
||||
if (!activePopover?.hideTimer) return;
|
||||
window.clearTimeout(activePopover.hideTimer);
|
||||
activePopover.hideTimer = null;
|
||||
}
|
||||
|
||||
function destroyActivePopover(): void {
|
||||
if (!activePopover) return;
|
||||
const { popover, component, reposition, onPopoverEnter, onPopoverLeave, hideTimer } = activePopover;
|
||||
if (hideTimer) window.clearTimeout(hideTimer);
|
||||
popover.removeEventListener("mouseenter", onPopoverEnter);
|
||||
popover.removeEventListener("mouseleave", onPopoverLeave);
|
||||
window.removeEventListener("scroll", reposition, true);
|
||||
window.removeEventListener("resize", reposition);
|
||||
component.unload();
|
||||
popover.remove();
|
||||
activePopover = null;
|
||||
}
|
||||
|
|
|
|||
211
src/main.ts
211
src/main.ts
|
|
@ -27,6 +27,13 @@ type SelectionSnapshot = {
|
|||
to: any;
|
||||
};
|
||||
|
||||
type AppWithCommands = obsidian.App & {
|
||||
commands?: {
|
||||
commands?: Record<string, { name: string }>;
|
||||
executeCommandById?: (commandId: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const ZOTERO_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect x="10" y="10" width="80" height="80" rx="10" fill="currentColor" opacity="0.12"/>
|
||||
<text x="50" y="70" font-size="58" text-anchor="middle" font-family="serif" fill="currentColor" font-weight="bold">Z</text>
|
||||
|
|
@ -86,7 +93,6 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
editorExtension: any = null;
|
||||
ribbonIconEl: HTMLElement | null = null;
|
||||
titlebarActions: WeakMap<any, HTMLElement[]> = new WeakMap();
|
||||
styleEl: HTMLStyleElement | null = null;
|
||||
focusBurstTimer: number | null = null;
|
||||
focusBurstStopTimer: number | null = null;
|
||||
focusBurstTopmostResetTimer: number | null = null;
|
||||
|
|
@ -161,13 +167,11 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
});
|
||||
|
||||
this.addSettingTab(new ZoteroSettingTab(this.app, this));
|
||||
this.injectStyles();
|
||||
this.applyLanguage();
|
||||
|
||||
this.registerEvent(this.app.workspace.on("active-leaf-change", () => this.refreshToolbars()));
|
||||
this.registerEvent(this.app.workspace.on("layout-change", () => this.refreshToolbars()));
|
||||
|
||||
window.setTimeout(() => this.refreshToolbars(), 100);
|
||||
this.app.workspace.onLayoutReady(() => this.refreshToolbars());
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
@ -184,9 +188,6 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
this.clearToolbar(leaf.view);
|
||||
}
|
||||
});
|
||||
|
||||
this.styleEl?.remove();
|
||||
this.styleEl = null;
|
||||
}
|
||||
|
||||
// ══ Settings ══════════════════════════════════════════════════════════════
|
||||
|
|
@ -213,23 +214,22 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
}
|
||||
|
||||
getCommandLabels() {
|
||||
const prefix = "Zotero Citations: ";
|
||||
return {
|
||||
"insert-edit-citation": prefix + this.t("command.insertCitation"),
|
||||
"toggle-word-display": prefix + this.t("command.toggleWordDisplay"),
|
||||
"toggle-toolbar": prefix + this.t("command.toggleToolbar"),
|
||||
"insert-bibliography": prefix + this.t("command.insertBibliography"),
|
||||
"refresh-citations": prefix + this.t("command.refreshCitations"),
|
||||
"export-to-word": prefix + this.t("command.exportToWord"),
|
||||
"unlink-citations": prefix + this.t("command.unlinkCitations"),
|
||||
"document-preferences": prefix + this.t("command.documentPreferences"),
|
||||
"check-pandoc": prefix + this.t("command.checkPandoc"),
|
||||
"insert-edit-citation": this.t("command.insertCitation"),
|
||||
"toggle-word-display": this.t("command.toggleWordDisplay"),
|
||||
"toggle-toolbar": this.t("command.toggleToolbar"),
|
||||
"insert-bibliography": this.t("command.insertBibliography"),
|
||||
"refresh-citations": this.t("command.refreshCitations"),
|
||||
"export-to-word": this.t("command.exportToWord"),
|
||||
"unlink-citations": this.t("command.unlinkCitations"),
|
||||
"document-preferences": this.t("command.documentPreferences"),
|
||||
"check-pandoc": this.t("command.checkPandoc"),
|
||||
};
|
||||
}
|
||||
|
||||
syncCommandLabels() {
|
||||
const labels = this.getCommandLabels();
|
||||
const commands = (this.app as any).commands?.commands ?? {};
|
||||
const commands = (this.app as AppWithCommands).commands?.commands ?? {};
|
||||
|
||||
for (const [id, name] of Object.entries(labels)) {
|
||||
const fullId = `${this.manifest.id}:${id}`;
|
||||
|
|
@ -253,7 +253,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
|
||||
/** Force Markdown views to re-render so Word-style footnote settings update everywhere. */
|
||||
refreshEditorExtension() {
|
||||
(this.app.workspace as any).updateOptions();
|
||||
this.app.workspace.updateOptions();
|
||||
this.refreshMarkdownPreviews();
|
||||
this.refreshToolbars();
|
||||
}
|
||||
|
|
@ -261,7 +261,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
refreshMarkdownPreviews() {
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view instanceof obsidian.MarkdownView) {
|
||||
(leaf.view as any).previewMode?.rerender(true);
|
||||
leaf.view.previewMode?.rerender(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -299,7 +299,9 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file) return null;
|
||||
|
||||
const base = (this.app.vault.adapter as any).getBasePath?.();
|
||||
const base = this.app.vault.adapter instanceof obsidian.FileSystemAdapter
|
||||
? this.app.vault.adapter.getBasePath()
|
||||
: null;
|
||||
return base ? `${base}/${file.path}` : file.path;
|
||||
}
|
||||
|
||||
|
|
@ -380,7 +382,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
}
|
||||
}
|
||||
try {
|
||||
if (process.platform === "darwin") {
|
||||
if (obsidian.Platform.isMacOS) {
|
||||
const { execFile } = require("child_process");
|
||||
execFile(
|
||||
"osascript",
|
||||
|
|
@ -418,7 +420,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
// noop
|
||||
}
|
||||
try {
|
||||
(this.app as any).commands.executeCommandById("editor:focus");
|
||||
(this.app as AppWithCommands).commands?.executeCommandById?.("editor:focus");
|
||||
} catch (_e) {
|
||||
// noop
|
||||
}
|
||||
|
|
@ -809,7 +811,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
refreshToolbars() {
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view instanceof obsidian.MarkdownView) {
|
||||
this.syncToolbar(leaf.view as any);
|
||||
this.syncToolbar(leaf.view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -821,7 +823,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
new obsidian.Notice(this.settings.showToolbar ? this.t("notice.toolbarShown") : this.t("notice.toolbarHidden"));
|
||||
}
|
||||
|
||||
syncToolbar(view: any) {
|
||||
syncToolbar(view: obsidian.MarkdownView) {
|
||||
this.clearToolbar(view);
|
||||
if (!this.settings.showToolbar) return;
|
||||
|
||||
|
|
@ -864,7 +866,7 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
this.titlebarActions.set(view, actionEls);
|
||||
}
|
||||
|
||||
clearToolbar(view: any) {
|
||||
clearToolbar(view: obsidian.MarkdownView) {
|
||||
view.contentEl.querySelectorAll(".zotero-toolbar").forEach((el: Element) => el.remove());
|
||||
const actionEls = this.titlebarActions.get(view);
|
||||
if (!actionEls) return;
|
||||
|
|
@ -927,160 +929,6 @@ export default class ZoteroCitations extends obsidian.Plugin {
|
|||
if (!locator) return locator;
|
||||
return locator.replace(/^(p\.|para\.|sec\.|ch\.|fig\.|table|v\.|l\.|n\.|col\.|no\.|vol\.)\s+/i, "");
|
||||
}
|
||||
|
||||
// ── CSS ───────────────────────────────────────────────────────────────────
|
||||
injectStyles() {
|
||||
this.styleEl?.remove();
|
||||
const s = document.createElement("style");
|
||||
s.id = "zotero-wi-styles";
|
||||
s.textContent = `
|
||||
/* Word-style footnote marker */
|
||||
.zotero-fn-widget {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sup.zotero-fn-num {
|
||||
line-height: 0;
|
||||
}
|
||||
sup.zotero-fn-num > .zotero-footnote-marker {
|
||||
color: var(--text-accent) !important;
|
||||
display: inline;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-text);
|
||||
pointer-events: auto;
|
||||
text-decoration: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
sup.zotero-fn-num > .zotero-footnote-marker:hover,
|
||||
sup.zotero-fn-num > .zotero-footnote-marker:focus-visible {
|
||||
color: var(--text-accent-hover, var(--text-accent)) !important;
|
||||
text-decoration: none !important;
|
||||
outline: none;
|
||||
}
|
||||
.zotero-fn-widget.zotero-fn-highlighted {
|
||||
background: var(--text-highlight-bg) !important;
|
||||
color: var(--text-normal) !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.zotero-fn-widget.zotero-fn-highlighted > sup.zotero-fn-num,
|
||||
.zotero-fn-widget.zotero-fn-highlighted > sup.zotero-fn-num > .zotero-footnote-marker,
|
||||
mark .zotero-rendered-footnote-sup,
|
||||
mark .zotero-rendered-footnote-ref {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.zotero-rendered-footnote-sup {
|
||||
line-height: 0;
|
||||
}
|
||||
a.zotero-rendered-footnote-ref {
|
||||
color: var(--text-accent);
|
||||
font-size: 0.75em;
|
||||
text-decoration: none;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.zotero-footnote-hover {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
max-width: min(32rem, calc(100vw - 16px));
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
box-shadow: var(--shadow-s);
|
||||
color: var(--text-normal);
|
||||
font-size: 0.9em;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
}
|
||||
.zotero-footnote-popover {
|
||||
max-width: min(36rem, calc(100vw - 16px));
|
||||
max-height: min(70vh, 42rem);
|
||||
overflow: auto;
|
||||
}
|
||||
.zotero-footnote-popover .markdown-embed {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.zotero-footnote-popover .markdown-embed-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.zotero-footnote-popover .markdown-preview-view {
|
||||
min-height: 0;
|
||||
padding: 0 12px 2px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.zotero-footnote-popover .markdown-preview-view > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.zotero-footnote-popover .markdown-preview-view > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.zotero-footnote-locator-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px 10px;
|
||||
}
|
||||
.zotero-footnote-locator-editor input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 26px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.9em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.zotero-footnote-locator-editor > div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.zotero-footnote-locator-editor button {
|
||||
height: 26px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.85em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
/* Connection status dots */
|
||||
.zotero-status-dot {
|
||||
display: inline-block; width: 10px; height: 10px;
|
||||
border-radius: 50%; margin-right: 8px; vertical-align: middle;
|
||||
}
|
||||
.zotero-status-ok { background: #4caf50; }
|
||||
.zotero-status-err { background: #f44336; }
|
||||
.zotero-status-unknown { background: #9e9e9e; }
|
||||
.zotero-status-row { display: flex; align-items: center; margin: 6px 0; }
|
||||
.view-action.zotero-titlebar-action.is-active {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
.view-action.zotero-titlebar-action.is-active svg {
|
||||
stroke: currentColor;
|
||||
}
|
||||
/* Reduce excessive top spacing in plugin modals so title aligns with close button */
|
||||
.modal:has(.zotero-prefs-modal),
|
||||
.modal:has(.zotero-unlink-modal),
|
||||
.modal:has(.zotero-search-modal) {
|
||||
padding-top: 12px;
|
||||
}
|
||||
.zotero-prefs-modal, .zotero-unlink-modal, .zotero-search-modal {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.zotero-prefs-modal h2:first-child,
|
||||
.zotero-unlink-modal h2:first-child,
|
||||
.zotero-search-modal h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
this.styleEl = s;
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmModal extends obsidian.Modal {
|
||||
|
|
@ -1099,10 +947,9 @@ class ConfirmModal extends obsidian.Modal {
|
|||
|
||||
onOpen() {
|
||||
if (this.cls) this.contentEl.addClass(this.cls);
|
||||
this.contentEl.createEl("h2", { text: this.title });
|
||||
this.contentEl.createEl("h2", { text: this.title, cls: "zotero-modal-title" });
|
||||
this.contentEl.createEl("p", { text: this.msg });
|
||||
const row = this.contentEl.createDiv({ cls: "zotero-btn-row" });
|
||||
row.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:16px";
|
||||
row.createEl("button", { text: appT(this.app, "common.cancel") }).addEventListener("click", () => this.close());
|
||||
const ok = row.createEl("button", { text: appT(this.app, "common.confirm"), cls: "mod-warning" });
|
||||
ok.addEventListener("click", () => {
|
||||
|
|
|
|||
|
|
@ -17,27 +17,20 @@ export class ExportModal extends Modal {
|
|||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl("h2", { text: appT(this.app, "export.chooseLocation") });
|
||||
contentEl.createEl("h2", { text: appT(this.app, "export.chooseLocation"), cls: "zotero-modal-title" });
|
||||
contentEl.createEl("p", {
|
||||
text: appT(this.app, "export.pathHint"),
|
||||
cls: "zotero-export-hint",
|
||||
});
|
||||
|
||||
this.input = contentEl.createEl("input", { type: "text" });
|
||||
this.input = contentEl.createEl("input", { type: "text", cls: "zotero-export-input" });
|
||||
this.input.value = this.suggestedPath;
|
||||
this.input.style.width = "100%";
|
||||
this.input.style.marginBottom = "12px";
|
||||
this.input.style.padding = "6px 10px";
|
||||
this.input.style.boxSizing = "border-box";
|
||||
this.input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") this.doConfirm();
|
||||
if (e.key === "Escape") this.close();
|
||||
});
|
||||
|
||||
const btnRow = contentEl.createDiv({ cls: "zotero-btn-row" });
|
||||
btnRow.style.display = "flex";
|
||||
btnRow.style.justifyContent = "flex-end";
|
||||
btnRow.style.gap = "8px";
|
||||
|
||||
const cancel = btnRow.createEl("button", { text: appT(this.app, "common.cancel") });
|
||||
cancel.addEventListener("click", () => this.close());
|
||||
|
|
|
|||
|
|
@ -40,55 +40,39 @@ export class PreferencesModal extends Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("zotero-prefs-modal");
|
||||
contentEl.createEl("h2", { text: appT(this.app, "prefs.title") });
|
||||
contentEl.createEl("h2", { text: appT(this.app, "prefs.title"), cls: "zotero-modal-title" });
|
||||
|
||||
// ── Style selector with search (Improvement 2) ──
|
||||
const styleWrap = contentEl.createDiv();
|
||||
styleWrap.style.marginBottom = "16px";
|
||||
const styleWrap = contentEl.createDiv({ cls: "zotero-prefs-section" });
|
||||
|
||||
const labelRow = styleWrap.createDiv();
|
||||
labelRow.style.display = "flex";
|
||||
labelRow.style.justifyContent = "space-between";
|
||||
labelRow.style.alignItems = "center";
|
||||
labelRow.style.marginBottom = "6px";
|
||||
const labelRow = styleWrap.createDiv({ cls: "zotero-prefs-label-row" });
|
||||
|
||||
labelRow.createEl("label", { text: appT(this.app, "prefs.styleLabel") });
|
||||
|
||||
const refreshBtn = labelRow.createEl("button", {
|
||||
text: appT(this.app, "prefs.refreshStyles"),
|
||||
cls: "clickable-icon",
|
||||
cls: "clickable-icon zotero-prefs-refresh",
|
||||
});
|
||||
refreshBtn.style.fontSize = "0.85em";
|
||||
refreshBtn.addEventListener("click", () => this.loadStyles(true));
|
||||
|
||||
// Search input for styles
|
||||
this.styleSearchInput = styleWrap.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: appT(this.app, "prefs.searchStylePlaceholder"),
|
||||
cls: "zotero-style-search-input",
|
||||
});
|
||||
this.styleSearchInput.style.width = "100%";
|
||||
this.styleSearchInput.style.marginBottom = "6px";
|
||||
this.styleSearchInput.style.padding = "4px 8px";
|
||||
this.styleSearchInput.addEventListener("input", () => this.filterStyles());
|
||||
|
||||
// Style list container
|
||||
this.styleListEl = styleWrap.createDiv({ cls: "zotero-style-list" });
|
||||
this.styleListEl.style.maxHeight = "200px";
|
||||
this.styleListEl.style.overflowY = "auto";
|
||||
this.styleListEl.style.border = "1px solid var(--background-modifier-border)";
|
||||
this.styleListEl.style.borderRadius = "4px";
|
||||
this.styleListEl.style.padding = "4px";
|
||||
|
||||
// Load styles (dynamic from Zotero + fallback)
|
||||
await this.loadStyles(false);
|
||||
|
||||
// ── Mode selector ──
|
||||
const modeWrap = contentEl.createDiv();
|
||||
modeWrap.style.marginBottom = "16px";
|
||||
modeWrap.createEl("label", { text: appT(this.app, "prefs.modeLabel") }).style.display = "block";
|
||||
const modeSelect = modeWrap.createEl("select");
|
||||
modeSelect.style.width = "100%";
|
||||
modeSelect.style.marginTop = "6px";
|
||||
const modeWrap = contentEl.createDiv({ cls: "zotero-prefs-section" });
|
||||
modeWrap.createEl("label", { text: appT(this.app, "prefs.modeLabel"), cls: "zotero-mode-label" });
|
||||
const modeSelect = modeWrap.createEl("select", { cls: "zotero-mode-select" });
|
||||
modeSelect.createEl("option", { text: getModeLabel("endnote", getAppSettings(this.app) || DEFAULT_SETTINGS, "option"), value: "endnote" });
|
||||
modeSelect.createEl("option", { text: getModeLabel("inline", getAppSettings(this.app) || DEFAULT_SETTINGS, "option"), value: "inline" });
|
||||
modeSelect.value = this.selectedMode;
|
||||
|
|
@ -99,20 +83,12 @@ export class PreferencesModal extends Modal {
|
|||
|
||||
// ── Citation count info ──
|
||||
const infoWrap = contentEl.createDiv({ cls: "zotero-prefs-info" });
|
||||
infoWrap.style.background = "var(--background-secondary)";
|
||||
infoWrap.style.padding = "10px";
|
||||
infoWrap.style.borderRadius = "4px";
|
||||
infoWrap.style.marginBottom = "16px";
|
||||
infoWrap.createEl("span", { text: appT(this.app, "prefs.citationCount") });
|
||||
this.citationsFoundEl = infoWrap.createEl("span", { text: "–", cls: "zotero-citation-count" });
|
||||
this.citationsFoundEl.style.fontWeight = "600";
|
||||
this.updateCitationCount();
|
||||
|
||||
// ── Buttons ──
|
||||
const btnRow = contentEl.createDiv({ cls: "zotero-btn-row" });
|
||||
btnRow.style.display = "flex";
|
||||
btnRow.style.gap = "8px";
|
||||
btnRow.style.justifyContent = "flex-end";
|
||||
const cancelBtn = btnRow.createEl("button", { text: appT(this.app, "common.cancel") });
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
const applyBtn = btnRow.createEl("button", { text: appT(this.app, "prefs.apply"), cls: "mod-cta" });
|
||||
|
|
@ -168,15 +144,8 @@ export class PreferencesModal extends Modal {
|
|||
this.styleListEl.empty();
|
||||
for (const style of styles) {
|
||||
const item = this.styleListEl.createDiv({ cls: "zotero-style-item" });
|
||||
item.style.padding = "4px 8px";
|
||||
item.style.cursor = "pointer";
|
||||
item.style.borderRadius = "3px";
|
||||
item.textContent = style.title;
|
||||
|
||||
if (style.id === this.selectedStyle) {
|
||||
item.style.background = "var(--background-modifier-active-hover)";
|
||||
item.style.fontWeight = "600";
|
||||
}
|
||||
item.toggleClass("is-selected", style.id === this.selectedStyle);
|
||||
|
||||
item.addEventListener("click", () => {
|
||||
this.selectedStyle = style.id;
|
||||
|
|
@ -187,12 +156,6 @@ export class PreferencesModal extends Modal {
|
|||
);
|
||||
this.updateCitationCount();
|
||||
});
|
||||
item.addEventListener("mouseover", () => {
|
||||
if (style.id !== this.selectedStyle) item.style.background = "var(--background-modifier-hover)";
|
||||
});
|
||||
item.addEventListener("mouseout", () => {
|
||||
if (style.id !== this.selectedStyle) item.style.background = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class SearchModal extends Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("zotero-search-modal");
|
||||
contentEl.createEl("h2", { text: appT(this.app, "search.title") });
|
||||
contentEl.createEl("h2", { text: appT(this.app, "search.title"), cls: "zotero-modal-title" });
|
||||
|
||||
const styleName = getStyleName(this.opts.style, getAppSettings(this.app) || DEFAULT_SETTINGS);
|
||||
contentEl.createEl("p", {
|
||||
|
|
@ -58,37 +58,24 @@ export class SearchModal extends Modal {
|
|||
placeholder: appT(this.app, "search.placeholder"),
|
||||
cls: "zotero-search-input",
|
||||
});
|
||||
this.searchInput.style.width = "100%";
|
||||
this.searchInput.addEventListener("input", () => this.onSearchInput());
|
||||
|
||||
this.resultsEl = contentEl.createDiv({ cls: "zotero-results" });
|
||||
this.resultsEl.style.maxHeight = "300px";
|
||||
this.resultsEl.style.overflowY = "auto";
|
||||
this.resultsEl.style.margin = "8px 0";
|
||||
this.resultsEl.createEl("p", {
|
||||
text: appT(this.app, "search.enterQuery"),
|
||||
cls: "zotero-results-placeholder",
|
||||
});
|
||||
|
||||
const pageWrap = contentEl.createDiv({ cls: "zotero-page-wrap" });
|
||||
pageWrap.style.display = "flex";
|
||||
pageWrap.style.alignItems = "center";
|
||||
pageWrap.style.gap = "8px";
|
||||
pageWrap.style.marginTop = "8px";
|
||||
pageWrap.createEl("label", { text: appT(this.app, "search.pageLabel") });
|
||||
this.pageInput = pageWrap.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: appT(this.app, "search.pagePlaceholder"),
|
||||
cls: "zotero-page-input",
|
||||
});
|
||||
this.pageInput.style.flex = "1";
|
||||
if (this.opts.existingPage) this.pageInput.value = this.opts.existingPage;
|
||||
|
||||
const btnRow = contentEl.createDiv({ cls: "zotero-btn-row" });
|
||||
btnRow.style.display = "flex";
|
||||
btnRow.style.justifyContent = "flex-end";
|
||||
btnRow.style.gap = "8px";
|
||||
btnRow.style.marginTop = "12px";
|
||||
|
||||
const cancelBtn = btnRow.createEl("button", { text: appT(this.app, "common.cancel") });
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
|
|
@ -141,9 +128,6 @@ export class SearchModal extends Modal {
|
|||
|
||||
private renderResultItem(item: ZoteroItem): void {
|
||||
const row = this.resultsEl.createDiv({ cls: "zotero-result-row" });
|
||||
row.style.padding = "6px 8px";
|
||||
row.style.cursor = "pointer";
|
||||
row.style.borderRadius = "4px";
|
||||
|
||||
const authors = item.creators
|
||||
.filter((c) => c.creatorType === "author")
|
||||
|
|
@ -155,29 +139,19 @@ export class SearchModal extends Modal {
|
|||
const typeLabel = SearchModal.typeLabel(item.itemType, getAppSettings(this.app) || DEFAULT_SETTINGS);
|
||||
|
||||
const titleDiv = row.createEl("div", { text: item.title, cls: "zotero-result-title" });
|
||||
titleDiv.style.fontWeight = "500";
|
||||
const metaDiv = row.createEl("div", {
|
||||
text: `${authors}${authors ? " · " : ""}${year} · ${typeLabel}`,
|
||||
cls: "zotero-result-meta",
|
||||
});
|
||||
metaDiv.style.color = "var(--text-muted)";
|
||||
|
||||
row.addEventListener("click", () => this.selectItem(item, row));
|
||||
row.addEventListener("mouseover", () => {
|
||||
row.style.background = "var(--background-modifier-hover)";
|
||||
});
|
||||
row.addEventListener("mouseout", () => {
|
||||
if (this.selectedItem?.key !== item.key) {
|
||||
row.style.background = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private selectItem(item: ZoteroItem, rowEl: HTMLElement): void {
|
||||
this.resultsEl.querySelectorAll(".zotero-result-row").forEach((el: Element) => {
|
||||
(el as HTMLElement).style.background = "";
|
||||
(el as HTMLElement).removeClass("is-selected");
|
||||
});
|
||||
rowEl.style.background = "var(--background-modifier-active-hover)";
|
||||
rowEl.addClass("is-selected");
|
||||
this.selectedItem = item;
|
||||
this.confirmBtn.disabled = false;
|
||||
|
||||
|
|
@ -190,11 +164,6 @@ export class SearchModal extends Modal {
|
|||
let previewEl = this.contentEl.querySelector(".zotero-preview") as HTMLElement | null;
|
||||
if (!previewEl) {
|
||||
previewEl = this.contentEl.createDiv({ cls: "zotero-preview" });
|
||||
previewEl.style.background = "var(--background-secondary)";
|
||||
previewEl.style.padding = "6px 10px";
|
||||
previewEl.style.borderRadius = "4px";
|
||||
previewEl.style.marginTop = "8px";
|
||||
previewEl.style.fontStyle = "italic";
|
||||
}
|
||||
previewEl.setText(appT(this.app, "search.preview", { preview }));
|
||||
|
||||
|
|
|
|||
|
|
@ -101,10 +101,9 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Zotero Citations" });
|
||||
|
||||
// ── Interface language ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.interface") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.interface")).setHeading();
|
||||
new Setting(containerEl)
|
||||
.setName(t(this.plugin.settings, "settings.interface"))
|
||||
.setDesc(t(this.plugin.settings, "settings.interfaceDesc"))
|
||||
|
|
@ -121,17 +120,19 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
// ── Connection status ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.connection") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.connection")).setHeading();
|
||||
const row = containerEl.createDiv({ cls: "zotero-status-row" });
|
||||
this.statusDot = row.createSpan({ cls: "zotero-status-dot zotero-status-unknown" });
|
||||
this.statusText = row.createSpan({ text: t(this.plugin.settings, "settings.checking") });
|
||||
const btn = containerEl.createEl("button", { text: t(this.plugin.settings, "settings.recheck") });
|
||||
btn.style.marginTop = "4px";
|
||||
const btn = containerEl.createEl("button", {
|
||||
text: t(this.plugin.settings, "settings.recheck"),
|
||||
cls: "zotero-settings-check-button",
|
||||
});
|
||||
btn.addEventListener("click", () => this.checkConnection());
|
||||
this.checkConnection();
|
||||
|
||||
// ── Citation style ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.citationStyleSection") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.citationStyleSection")).setHeading();
|
||||
new Setting(containerEl)
|
||||
.setName(t(this.plugin.settings, "settings.defaultStyle"))
|
||||
.setDesc(t(this.plugin.settings, "settings.defaultStyleDesc"))
|
||||
|
|
@ -176,7 +177,7 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
// ── Editor display ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.editorDisplaySection") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.editorDisplaySection")).setHeading();
|
||||
new Setting(containerEl)
|
||||
.setName(t(this.plugin.settings, "settings.wordDisplay"))
|
||||
.setDesc(t(this.plugin.settings, "settings.wordDisplayDesc"))
|
||||
|
|
@ -206,10 +207,6 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
// ── Improvement 3: Individual toolbar button toggles ──
|
||||
if (this.plugin.settings.showToolbar) {
|
||||
const toolbarSection = containerEl.createDiv({ cls: "zotero-toolbar-buttons-section" });
|
||||
toolbarSection.style.paddingLeft = "24px";
|
||||
toolbarSection.style.borderLeft = "2px solid var(--background-modifier-border)";
|
||||
toolbarSection.style.marginLeft = "12px";
|
||||
toolbarSection.style.marginBottom = "12px";
|
||||
|
||||
const buttonKeys: { key: keyof ToolbarButtons; labelKey: string }[] = [
|
||||
{ key: "export", labelKey: "settings.toolbarBtn.export" },
|
||||
|
|
@ -235,7 +232,7 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
// ── Export section ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.exportSection") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.exportSection")).setHeading();
|
||||
new Setting(containerEl)
|
||||
.setName(t(this.plugin.settings, "settings.pandocPath"))
|
||||
.setDesc(t(this.plugin.settings, "settings.pandocPathDesc"))
|
||||
|
|
@ -278,12 +275,11 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
// ── Command list ──
|
||||
containerEl.createEl("h3", { text: t(this.plugin.settings, "settings.commandsSection") });
|
||||
new Setting(containerEl).setName(t(this.plugin.settings, "settings.commandsSection")).setHeading();
|
||||
const cmds = Object.values(this.plugin.getCommandLabels()) as string[];
|
||||
const ul = containerEl.createEl("ul");
|
||||
for (const c of cmds) {
|
||||
const li = ul.createEl("li", { text: c });
|
||||
li.style.color = "var(--text-muted)";
|
||||
ul.createEl("li", { text: c, cls: "zotero-settings-command" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,7 +291,7 @@ export class ZoteroSettingTab extends PluginSettingTab {
|
|||
url: `http://127.0.0.1:${this.plugin.settings.zoteroPort}/connector/ping`,
|
||||
method: "GET",
|
||||
throw: false,
|
||||
} as any);
|
||||
});
|
||||
if (r.status === 200) {
|
||||
this.statusDot.className = "zotero-status-dot zotero-status-ok";
|
||||
this.statusText.textContent = t(this.plugin.settings, "status.connected");
|
||||
|
|
|
|||
316
styles.css
Normal file
316
styles.css
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
/* Word-style footnote marker */
|
||||
.zotero-fn-widget {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup.zotero-fn-num,
|
||||
.zotero-rendered-footnote-sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
sup.zotero-fn-num > .zotero-footnote-marker {
|
||||
color: var(--text-accent) !important;
|
||||
display: inline;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-text);
|
||||
pointer-events: auto;
|
||||
text-decoration: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
sup.zotero-fn-num > .zotero-footnote-marker:hover,
|
||||
sup.zotero-fn-num > .zotero-footnote-marker:focus-visible {
|
||||
color: var(--text-accent-hover, var(--text-accent)) !important;
|
||||
text-decoration: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.zotero-fn-widget.zotero-fn-highlighted {
|
||||
background: var(--text-highlight-bg) !important;
|
||||
color: var(--text-normal) !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.zotero-fn-widget.zotero-fn-highlighted > sup.zotero-fn-num,
|
||||
.zotero-fn-widget.zotero-fn-highlighted > sup.zotero-fn-num > .zotero-footnote-marker,
|
||||
mark .zotero-rendered-footnote-sup,
|
||||
mark .zotero-rendered-footnote-ref {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
a.zotero-rendered-footnote-ref {
|
||||
color: var(--text-accent);
|
||||
font-size: 0.75em;
|
||||
text-decoration: none;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
max-width: min(36rem, calc(100vw - 16px));
|
||||
max-height: min(70vh, 42rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover .markdown-embed {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover .markdown-embed-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover .markdown-preview-view {
|
||||
min-height: 0;
|
||||
padding: 0 12px 2px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover .markdown-preview-view > *:first-child,
|
||||
.zotero-modal-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.zotero-footnote-popover .markdown-preview-view > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.zotero-footnote-locator-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px 10px;
|
||||
}
|
||||
|
||||
.zotero-footnote-locator-editor input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 26px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.9em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.zotero-footnote-locator-editor > div,
|
||||
.zotero-btn-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.zotero-footnote-locator-editor button {
|
||||
height: 26px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.85em;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.zotero-status-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.zotero-status-ok {
|
||||
background: var(--color-green);
|
||||
}
|
||||
|
||||
.zotero-status-err {
|
||||
background: var(--color-red);
|
||||
}
|
||||
|
||||
.zotero-status-unknown {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.zotero-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.zotero-settings-check-button {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.zotero-toolbar-buttons-section {
|
||||
padding-left: 24px;
|
||||
border-left: 2px solid var(--background-modifier-border);
|
||||
margin-left: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.zotero-settings-command {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Title bar icons */
|
||||
.view-action.zotero-titlebar-action.is-active {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.view-action.zotero-titlebar-action.is-active svg {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal:has(.zotero-prefs-modal),
|
||||
.modal:has(.zotero-unlink-modal),
|
||||
.modal:has(.zotero-search-modal) {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.zotero-prefs-modal,
|
||||
.zotero-unlink-modal,
|
||||
.zotero-search-modal {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.zotero-prefs-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.zotero-prefs-label-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.zotero-prefs-refresh {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.zotero-style-search-input,
|
||||
.zotero-mode-select,
|
||||
.zotero-search-input,
|
||||
.zotero-export-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.zotero-style-search-input {
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.zotero-style-list,
|
||||
.zotero-results {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.zotero-style-list {
|
||||
max-height: 200px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.zotero-mode-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zotero-mode-select {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.zotero-prefs-info,
|
||||
.zotero-preview {
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.zotero-prefs-info {
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.zotero-citation-count {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zotero-btn-row {
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.zotero-style-item,
|
||||
.zotero-result-row {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.zotero-style-item {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.zotero-style-item:hover,
|
||||
.zotero-result-row:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.zotero-style-item.is-selected,
|
||||
.zotero-result-row.is-selected {
|
||||
background: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.zotero-style-item.is-selected {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.zotero-results {
|
||||
max-height: 300px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.zotero-page-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.zotero-page-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zotero-result-row {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.zotero-result-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.zotero-result-meta,
|
||||
.zotero-results-placeholder,
|
||||
.zotero-style-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.zotero-preview {
|
||||
padding: 6px 10px;
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.zotero-export-input {
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"0.2.0": "1.5.7"
|
||||
"0.2.0": "1.5.7",
|
||||
"0.2.1": "1.5.7"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue