moyf_easy-copy/src/settingTab.ts
Andre Light 4ac623b1bd feat: opt-in toggle, editor-paste cooperation, and orphaned-setting fix
Addresses #35 maintainer feedback: gate paste-time resolution behind a
default-off toggle with 5-min TTL on lastCopyMeta. Switch from DOM
paste (capture phase) to editor-paste workspace event with documented
defaultPrevented yield protocol; register only when active.

Also fixes a pre-existing bug where simplifiedHeadingToNoteLink was
never read — the simplification fired unconditionally when filename
matched heading.

Designed, carefully reviewed, and edited by @lightmotive in collaboration with Claude Code.
2026-04-27 20:46:42 -06:00

451 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
App,
PluginSettingTab,
Setting,
requireApiVersion,
} from "obsidian";
import * as ObsidianModule from "obsidian";
import EasyCopy from "./main";
import { LinkFormat, BlockIdInsertPosition } from "./type";
interface SettingsContainer {
addSetting(cb: (setting: Setting) => void): void;
}
function createSettingsGroup(containerEl: HTMLElement, heading?: string): SettingsContainer {
// Check if SettingGroup is available (API 1.11.0+)
// requireApiVersion is the official Obsidian API method for version checking
if (requireApiVersion('1.11.0')) {
// Use SettingGroup - it's guaranteed to exist if requireApiVersion returns true
// Access SettingGroup via type assertion since it may not be in type definitions
// for older TypeScript versions, but exists at runtime in Obsidian 1.11.0+
// Using unknown instead of any to satisfy eslint while maintaining type safety
const SettingGroupClass = (ObsidianModule as unknown as { SettingGroup?: new (containerEl: HTMLElement) => {
setHeading(heading: string): {
addSetting(cb: (setting: Setting) => void): void;
};
addSetting(cb: (setting: Setting) => void): void;
} }).SettingGroup;
if (SettingGroupClass) {
const group = heading
? new SettingGroupClass(containerEl).setHeading(heading)
: new SettingGroupClass(containerEl);
return {
addSetting(cb: (setting: Setting) => void) {
group.addSetting(cb);
}
};
}
}
// Fallback path (either API < 1.11.0 or SettingGroup not found)
{
// Fallback: Create a heading manually for older API versions
// Note: While best practice prefers Setting.setHeading(), the fallback path
// is for versions that may not support it, so manual heading is appropriate here
if (heading) {
const headingEl = containerEl.createDiv('setting-group-heading');
headingEl.createEl('h3', { text: heading });
}
return {
addSetting(cb: (setting: Setting) => void) {
const setting = new Setting(containerEl);
cb(setting);
}
};
}
}
export class EasyCopySettingTab extends PluginSettingTab {
plugin: EasyCopy;
icon: string = 'copy-plus';
constructor(app: App, plugin: EasyCopy) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
const generalGroup = createSettingsGroup(containerEl);
generalGroup.addSetting(setting => setting
.setName(this.plugin.t('add-to-menu'))
.setDesc(this.plugin.t('add-to-menu-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.addToMenu)
.onChange(async (value) => {
this.plugin.settings.addToMenu = value;
await this.plugin.saveSettings();
})));
generalGroup.addSetting(setting => setting
.setName(this.plugin.t('add-extra-commands'))
.setDesc(this.plugin.t('add-extra-commands-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.addExtraCommands)
.onChange(async (value) => {
this.plugin.settings.addExtraCommands = value;
await this.plugin.saveSettings();
})));
generalGroup.addSetting(setting => setting
.setName(this.plugin.t('show-notice'))
.setDesc(this.plugin.t('show-notice-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNotice)
.onChange(async (value) => {
this.plugin.settings.showNotice = value;
await this.plugin.saveSettings();
})));
const formatGroup = createSettingsGroup(containerEl, this.plugin.t('format'));
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('link-format'))
.setDesc(this.plugin.t('link-format-desc'))
.addDropdown(dropdown => dropdown
.addOption(LinkFormat.OBSIDIAN, this.plugin.t('link-format-obsidian'))
.addOption(LinkFormat.MDLINK, this.plugin.t('markdown-link'))
.addOption(LinkFormat.WIKILINK, this.plugin.t('wiki-link'))
.setValue(this.plugin.settings.linkFormat)
.onChange(async (value) => {
this.plugin.settings.linkFormat = value as LinkFormat;
await this.plugin.saveSettings();
this.plugin.syncPasteHandlerRegistration();
this.display();
})));
// Only relevant when linkFormat is OBSIDIAN; the resolver intercepts paste
// to regenerate the path (shortest/relative/absolute) for the destination file.
if (this.plugin.settings.linkFormat === LinkFormat.OBSIDIAN) {
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('resolve-link-path-on-paste'))
.setDesc(this.plugin.t('resolve-link-path-on-paste-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.resolveLinkPathOnPaste)
.onChange(async (value) => {
this.plugin.settings.resolveLinkPathOnPaste = value;
await this.plugin.saveSettings();
this.plugin.syncPasteHandlerRegistration();
})));
}
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('use-heading-as-display'))
.setDesc(this.plugin.t('use-heading-as-display-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useHeadingAsDisplayText)
.onChange(async (value) => {
this.plugin.settings.useHeadingAsDisplayText = value;
await this.plugin.saveSettings();
this.display();
})));
// 新增:标题链接连接符设置,仅在禁用"使用标题作为显示文本"时显示
if (!this.plugin.settings.useHeadingAsDisplayText) {
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('heading-link-separator'))
.setDesc(this.plugin.t('heading-link-separator-desc'))
.addText(text => text
.setPlaceholder('#')
.setValue(this.plugin.settings.headingLinkSeparator)
.onChange(async (value) => {
this.plugin.settings.headingLinkSeparator = value || '#';
await this.plugin.saveSettings();
})
));
}
// 后续新增:文件名包含标题时,简化为复制文件链接(通常用于复制一级标题时)
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('simplified-heading-to-note-link'))
.setDesc(this.plugin.t('simplified-heading-to-note-link-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.simplifiedHeadingToNoteLink)
.onChange(async (value) => {
this.plugin.settings.simplifiedHeadingToNoteLink = value;
await this.plugin.saveSettings();
this.display();
})));
if (this.plugin.settings.simplifiedHeadingToNoteLink) {
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('strict-heading-match'))
.setDesc(this.plugin.t('strict-heading-match-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.strictHeadingMatch)
.onChange(async (value) => {
this.plugin.settings.strictHeadingMatch = value;
await this.plugin.saveSettings();
})));
}
// 新增:是否使用 frontmatter 属性作为显示文本
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('use-frontmatter-as-display'))
.setDesc(this.plugin.t('use-frontmatter-as-display-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useFrontmatterAsDisplay)
.onChange(async (value) => {
this.plugin.settings.useFrontmatterAsDisplay = value;
await this.plugin.saveSettings();
this.display();
})
));
// 新增:自定义 frontmatter 属性名,仅在上方开启时显示
if (this.plugin.settings.useFrontmatterAsDisplay) {
formatGroup.addSetting(setting => setting
.setName(this.plugin.t('frontmatter-key'))
.setDesc(this.plugin.t('frontmatter-key-desc'))
.addText(text => text
.setPlaceholder('title')
.setValue(this.plugin.settings.frontmatterKey)
.onChange(async (value) => {
this.plugin.settings.frontmatterKey = value || 'title';
await this.plugin.saveSettings();
})
));
}
const blockIdGroup = createSettingsGroup(containerEl, this.plugin.t('block-id'));
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('auto-add-block-id'))
.setDesc(this.plugin.t('auto-add-block-id-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoAddBlockId)
.onChange(async (value) => {
this.plugin.settings.autoAddBlockId = value;
await this.plugin.saveSettings();
this.display();
})));
if (this.plugin.settings.autoAddBlockId) {
// 新增块ID插入位置设置
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('block-id-insert-position'))
.setDesc(this.plugin.t('block-id-insert-position-desc'))
.addDropdown(dropdown => dropdown
.addOption(BlockIdInsertPosition.END_OF_BLOCK, this.plugin.t('block-id-end-of-block'))
.addOption(BlockIdInsertPosition.NEXT_LINE, this.plugin.t('block-id-next-line'))
.setValue(this.plugin.settings.blockIdInsertPosition)
.onChange(async (value) => {
this.plugin.settings.blockIdInsertPosition = value as BlockIdInsertPosition;
await this.plugin.saveSettings();
})));
}
if (this.plugin.settings.autoAddBlockId) {
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('manual-block-id'))
.setDesc(this.plugin.t('manual-block-id-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.allowManualBlockId)
.onChange(async (value) => {
this.plugin.settings.allowManualBlockId = value;
await this.plugin.saveSettings();
})));
}
// 新增:自动为 Block 链接添加显示文本
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('auto-block-display-text'))
.setDesc(this.plugin.t('auto-block-display-text-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoBlockDisplayText)
.onChange(async (value) => {
this.plugin.settings.autoBlockDisplayText = value;
await this.plugin.saveSettings();
this.display();
})
));
// 新增:块显示文本限制设置,仅在启用 autoBlockDisplayText 时显示
if (this.plugin.settings.autoBlockDisplayText) {
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('block-display-word-limit'))
.setDesc(this.plugin.t('block-display-word-limit-desc'))
.addText(text => text
.setPlaceholder('3')
.setValue(String(this.plugin.settings.blockDisplayWordLimit))
.onChange(async (value) => {
const numValue = parseInt(value) || 3;
this.plugin.settings.blockDisplayWordLimit = Math.max(1, numValue);
await this.plugin.saveSettings();
})
));
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('block-display-char-limit'))
.setDesc(this.plugin.t('block-display-char-limit-desc'))
.addText(text => text
.setPlaceholder('5')
.setValue(String(this.plugin.settings.blockDisplayCharLimit))
.onChange(async (value) => {
const numValue = parseInt(value) || 5;
this.plugin.settings.blockDisplayCharLimit = Math.max(1, numValue);
await this.plugin.saveSettings();
})
));
}
const targetGroup = createSettingsGroup(containerEl, this.plugin.t('target'));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('customize-targets'))
.setDesc(this.plugin.t('customize-targets-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.customizeTargets)
.onChange(async (value) => {
this.plugin.settings.customizeTargets = value;
await this.plugin.saveSettings();
this.display();
})));
// 只有当自定义复制对象选项开启时才显示具体的复制对象选项
if (this.plugin.settings.customizeTargets) {
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-inline-code'))
.setDesc(this.plugin.t('enable-inline-code-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableInlineCode)
.onChange(async (value) => {
this.plugin.settings.enableInlineCode = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-bold'))
.setDesc(this.plugin.t('enable-bold-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableBold)
.onChange(async (value) => {
this.plugin.settings.enableBold = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-highlight'))
.setDesc(this.plugin.t('enable-highlight-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableHighlight)
.onChange(async (value) => {
this.plugin.settings.enableHighlight = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-italic'))
.setDesc(this.plugin.t('enable-italic-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableItalic)
.onChange(async (value) => {
this.plugin.settings.enableItalic = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-strikethrough'))
.setDesc(this.plugin.t('enable-strikethrough-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableStrikethrough)
.onChange(async (value) => {
this.plugin.settings.enableStrikethrough = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-inline-latex'))
.setDesc(this.plugin.t('enable-inline-latex-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableInlineLatex)
.onChange(async (value) => {
this.plugin.settings.enableInlineLatex = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-link'))
.setDesc(this.plugin.t('enable-link-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableLink)
.onChange(async (value) => {
this.plugin.settings.enableLink = value;
await this.plugin.saveSettings();
})));
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-wikilink'))
.setDesc(this.plugin.t('enable-wikilink-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableWikiLink ?? true)
.onChange(async (value) => {
this.plugin.settings.enableWikiLink = value;
await this.plugin.saveSettings();
this.display(); // 切换后刷新界面以显示/隐藏下方选项
})));
}
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('enable-callout-copy'))
.setDesc(this.plugin.t('enable-callout-copy-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableCalloutCopy ?? true)
.onChange(async (value) => {
this.plugin.settings.enableCalloutCopy = value;
await this.plugin.saveSettings();
this.display();
})));
// 优先复制 Callout 内容
if (this.plugin.settings.enableCalloutCopy) {
targetGroup.addSetting(setting => setting
.setName(this.plugin.t('callout-copy-priority'))
.setDesc(this.plugin.t('callout-copy-priority-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.calloutCopyPriority ?? true)
.onChange(async (value) => {
this.plugin.settings.calloutCopyPriority = value;
await this.plugin.saveSettings();
})));
}
const specialFormatGroup = createSettingsGroup(containerEl, this.plugin.t('special-format'));
// 块链接特殊格式选项
specialFormatGroup.addSetting(setting => setting
.setName(this.plugin.t('auto-embed-block-link'))
.setDesc(this.plugin.t('auto-embed-block-link-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoEmbedBlockLink ?? false)
.onChange(async (value) => {
this.plugin.settings.autoEmbedBlockLink = value;
await this.plugin.saveSettings();
})));
// 仅当启用 Wiki 链接复制时显示
if (this.plugin.settings.enableWikiLink) {
specialFormatGroup.addSetting(setting => setting
.setName(this.plugin.t('keep-wiki-brackets'))
.setDesc(this.plugin.t('keep-wiki-brackets-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.keepWikiBrackets ?? true)
.onChange(async (value) => {
this.plugin.settings.keepWikiBrackets = value;
await this.plugin.saveSettings();
})));
}
}
}