居家隔离,卷了个Obsidian 插件

2,242 阅读10分钟

背景

最近在使用一款 Markdown 笔记应用: Obsidian,加之先前做了一个可以快捷收藏/跳转地址的工具: efficient-tools,于是这次打算撸个 Obsidian 的插件,借用 Obsidian 给这个小功能提供可视化界面操作的能力(赋。。。。能?)。

开始

efficient-tools 中,我们可以通过命令行的方式添加、删除以及查看并跳转链接,既然如此,那在这个插件中也需要实现这三种功能。

准备工作

在开始开发之前,先要做如下准备工作:

  1. Obsidian 官网 下载 Obsidian 应用;
  2. 参照 创建你的第一个插件 这篇文档准备好开发环境,并将插件名称改为 obsidian-link-keeper
  3. 由于 Obsidian 的插件在开发过程中只能通过停用/启用插件的方式让最新的代码生效,为了避开这个繁琐的过程,我们可以使用 Hot-Relod 这个工具来让我们的插件可以热重载。

在做完上述准备工作后,我们可以得到如下目录结构:

├── README.md
├── esbuild.config.mjs
├── main.ts // 入口文件
├── manifest.json // 插件的基本信息
├── modals.ts // 用于创建模态框
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── styles.css // 样式文件
├── tsconfig.json
├── version-bump.mjs
└── versions.json

设置filepath

efficient-tools 中,新增的地址会默认保存到 ${process.env.HOME}/etl.json 文件中,在 Obsidian 中,我们可以通过给插件添加配置的方式来设置保存地址的文件路径。

Obsidian 中提供了 Setting 类以创建诸如文本框,下拉框,滑动条,按钮等表单控件,以及 PluginSettingTab 类可以创建插件的配置 tab。

接下来,我们先在项目的根目录下创建 settings.ts 文件,用于自定义插件的设置:

// settings.ts

// 导入插件
import LinkKeeper from './main'

import { App, PluginSettingTab, Setting } from 'obsidian'

// 创建插件的自定义配置
export class LinkKeeperSettingTab extends PluginSettingTab {
  plugin: LinkKeeper
  
  /**
  * 构造函数中接受两个参数
  * app: Obsidian 中的App对象
  * plugin: 需要自定义设置的插件对象
  */
  constructor (app: App, plugin: LinkKeeper) {
    super (app, plugin)
    this.plugin = plugin
  }

  display(): void {
    // containerEl 是插件设置面板的容器
    const { containerEl } = this
    // 清空面板
    containerEl.empty()
    // 添加控件到面板容器中
    new Setting(containerEl)
      .setName("Link Filepath") // 设置控件名称
      .setDesc("The file where saves the links.") // 设置控件描述文案
      .addText((text) => // addText 方法用于创建 input 文本框, 回调函数中的参数为文本框dom对象
        text
          .setPlaceholder("Enter the full filepath") // 设置文本框 placeholder
          .setValue(this.plugin.settings.filepath) // 设置文本框内容
          .onChange(async (value) => { // 监听文本框的 change 事件
            this.plugin.settings.filepath = value
            await this.plugin.saveSettings() // 保存设置
          })
      )
  }
}

在自定义设置时需要引入自定义插件,以便将设置绑定到对应插件上。打开插件的入口文件 main.ts ,由于拉取的插件模板中存在其他示例内容,我们先“清理”一下 main.ts 文件,只保留我们需要的内容:

// main.ts

// 引入 Plugin 类
import { Plugin } from "obsidian"

// 创建自定义插件
export default class InsertLinkPlugin extends Plugin {
  async onload() {
     // onload 方法在插件被启用时调用
  }
}

💡 注:

在 Obsidian 中,自定义插件的类继承自插件基类 Plugin。插件存在两个生命周期函数:onload 以及 onunload,分别在组件启用时以及禁用时调用,具体可以参考插件剖析

"清理"完成后,我们需要在 main.ts 中引入插件设置,并在 onload 阶段将自定义设置注册到插件上:

// main.ts

import { Plugin } from "obsidian"
// 引入自定义设置
import { LinkKeeperSettingTab } from './settings'

// 定义自定义设置的类型
interface LinkKeeperSettings {
  filepath: string
}
// 定义自定义设置的默认值
const DEFAULT_SETTINGS: Partial<LinkKeeperSettings> = {
  filepath: `${process.env.HOME}/etl.json`
}

export default class LinkKeeperPlugin extends Plugin {
  settings: LinkKeeperSettings
  
  async onload() {
    // 加载自定义设置
    await this.loadSettings()
    // 将插件的配置 tab 添加到设置菜单中
    this.addSettingTab(new LinkKeeperSettingTab(this.app, this))
  }
  
  async loadSettings () {
    // 如果修改了配置项,可以覆盖默认配置
    this.settings = {
      ...DEFAULT_SETTINGS,
      ...await this.loadData()
    }
  }
  
  // 用于保存设置,暴露给自定义设置类中使用
  async saveSettings () {
    await this.saveData(this.settings)
  }
}

做完上述步骤后,执行 pnpm run dev,之后打开设置界面,启用自定义插件,即可在设置中看到插件的自定义配置了:

到这里,我们已经完成了第一步,接下来我们就可以开始着手实现我们的功能了~

添加链接

要想添加链接,我们需要一个添加链接的界面。我们来设想下这个界面里需要实现哪些功能:

  1. 首先我们需要添加两个输入框用于设置链接的名称以及地址;
  2. 之后再添加一个按钮,点击后对输入的内容进行校验并保存;
  3. 如果遇到异常场景,比如文件不存在,输入内容为空等情况,我们需要给出友好的弹窗提示。

好,功能已经明确了,那我们就来逐步实现它们,冲冲冲!

第一步:添加链接的界面

上文中提到的 界面,我们可以使用 Obsidian 提供的 Modal 来实现。Modal 用于创建一个模态框,在模态框中,我们可以通过 Setting 以及 createEl 的方式来自定义所需要显示的表单控件或者自定义元素内容。

首先,我们在项目根目录下新建 modals.ts 文件,用于创建模态框:

// modal.ts

import { App, Modal } from "obsidian"

// 创建添加地址的模态框
export class AddLink extends Modal {
  linkName: string // 用于保存链接名称
  linkUrl: string // 用于保存链接地址

  constructor(
    app: App
  ) {
    super(app)
  }
  // 在模态框打开时调用
  onOpen (): void {
  }
  // 在模态框关闭时调用
  onClose(): void {
    // 需要清空模态框的内容
    this.contentEl.empty()
  }
}

第二步:添加名称 && 地址输入框

通过 Obsidian 提供的 Setting 类,我们可以很方便的创建两个用于输入的文本框。创建文本框需要用到 Setting 中的 addText 方法。

onOpen 方法中,我们插入以下代码

// modal.ts

onOpen (): void {
  // contentEl 是模态框的 Element 对象
  const { contentEl } = this
  // 设置模态框标题
  contentEl.createEl("h1", { text: "Add Link", cls: "title" })
  // 创建用于设置地址名称的 input 框
  new Setting(contentEl).setName("Link name").addText(text => 
    text.setValue(this.linkName).setPlaceholder('name').onChange((value) => {
      this.linkName = value
    })
  )
  // 创建用于设置地址 url 的 input 框
  new Setting(contentEl).setName("Link url").addText(text =>
    text.setValue(this.linkUrl).setPlaceholder('url').onChange((value) => {
      this.linkUrl = value
    })
  )
}

💡 注:

createEl 方法用于自定义 Html 标签,其中 cls 用于设置标签的 class 属性,可以用于设置标签样式。样式统一设置在项目根目录的 style.css 文件中。

第三步:添加保存按钮

添加按钮需要用到 Setting 中的 addButton 方法。

onOpen 方法中继续添加以下内容:

// modal.ts

onOpen (): void {
  // 省略上述代码
  
  // 创建一个按钮
  new Setting(contentEl).addButton((btn) =>
    btn
      .setButtonText('Add') // 设置按钮文案
      .setCta()
      .onClick(() => {
        // 按钮的点击事件
      })
  )
}

第四步:实现校验 && 保存功能

接下来,在点击按钮时,我们需要对文本框中的内容进行校验,并给出提示。在这里我们仅校验文本框中是否输入内容,如果文本框中的内容为空,则给出 popup 提示,否则就保存输入的内容。

Obsidian 中提供了 Notice 类用于生成提示。由于提示的 popup 可能会在多出使用到,这里我们将其抽取到单独的 utils.ts 文件中:

// utils.ts

import { Notice } from 'obsidian'

export const noticeHandler = (msg: string) => new Notice(msg) 

之后,我们在 modals.ts 文件中引入,并添加到检验判断中去:

// modal.ts

import { noticeHandler } from './utils'

// 省略一大波代码

onOpen (): void {
  
  // 省略一小波代码
  
  new Setting(contentEl).addButton((btn) =>
    btn
      .setButtonText('Add') // 设置按钮文案
      .setCta()
      .onClick(() => {
        // 在上述 input 的 onChange 事件中已经保存了 linkName 以及 linkUrl
        // 在这里我们可以直接获取进行校验
        const { linkName, linkUrl } = this
        if (!(linkName.trim())) {
          noticeHandler('Link name is required!')
        } else if (!(linkUrl.trim())) {
          noticeHandler('Link url is required!')
        }
      })
  )
}

通过校验后,我们需要对填入的值进行保存,所谓的保存其实就是将值写入文件中。为了保证模态框模块的纯净,我们将保存的逻辑放在 main.ts 中完成。所以,在 main.ts 中调用这个 Modal 的时候,需要传入一个回调函数,用于保存链接设置。同时,在 main.ts 中需要通过 addCommand 方法创建自定义指令,用于呼起模态框。

打开 main.ts,添加保存地址配置的方法,并且在 onload 方法中创建自定义指令的逻辑:

// main.ts

import { Plugin, Editor } from "obsidian"
import { AddLink } from "./modals"
import { LinkKeeperSettingTab } from './settings'
import { readFile, writeFile } from 'fs/promises'
import { noticeHandler } from './utils'
interface LinkKeeperSettings {
  filepath: string
}

interface Options {
  [key: string]: string
}

const DEFAULT_SETTINGS: Partial<LinkKeeperSettings> = {
  filepath: `${process.env.HOME}/etl.json`
}

export default class LinkKeeperPlugin extends Plugin {
  settings: LinkKeeperSettings

  /**
   * get all links
   * @param cb 
   */
  async getLinks (cb: (data: Options) => void) {
    try {
      const data = await readFile(this.settings.filepath, { encoding: 'utf-8'})
      cb(JSON.parse(data || '{}'))
    } catch (err) {
      noticeHandler(err.message)
    }
  }
  
  /**
   * save link
   * @param data 
   * @param message 
   */
  async saveLink (data: Options, message: string) {
    try {
      await writeFile(this.settings.filepath, JSON.stringify(data))
      noticeHandler(message)
    } catch (err) {
      noticeHandler(err.message)
    }
  }

  /**
   * add link submission
   * @param name 
   * @param url 
   */
  onSubmit (name: string, url: string) {
    this.getLinks(async (data: Options) => {
      if (Object.prototype.toString.call(data) === '[object Object]') {
        this.saveLink({...data, [name]: url}, 'Add Link successfully!')
      } else {
        noticeHandler('Data format error! It must be a json object.')
      }
    })
  }

  /**
   * init modal
   * @param type 
   * @param options 
   * @returns 
   */
  initModal (type: string) {
    switch (type) {
      case 'addLink':
        return new AddLink(this.app, this.onSubmit.bind(this))

      default: break
    }
  }

  async onload() {
    // 添加呼起模态框的逻辑
    this.addCommand({
      id: "add-link", // 用于设置模态框 id
      name: "Add link", // 用于设置模态框名称
      callback: () => { // 执行命令的回调函数
        // 打开模态框
        this.initModal('addLink').open()
      }
    })
  }
}

这里我们添加了 onSubmit 方法,提供给模态框使用,用于保存地址设置。回到 modal.ts 文件中,我们需要在按钮的回调事件中加入保存地址的方法:

// modal.ts

export class AddLink extends Modal {
  linkName: string
  linkUrl: string

  onSubmit: (linkName: string, linkUrl: string) => void

  constructor(
    app: App,
    onSubmit: (linkName: string, linkUrl: string) => void
  ) {
    // 省略一小波代码
    
    // 绑定传过来的 onSubmit 方法
    this.onSubmit = onSubmit
  }

  onOpen (): void {
    // 省略一小波代码
    new Setting(contentEl).addButton((btn) =>
      btn
        .setButtonText('Add')
        .setCta()
        .onClick(() => {
          const { linkName, linkUrl } = this
          if (!(linkName.trim())) {
            noticeHandler('Link name is required!')
          } else if (!(linkUrl.trim())) {
            noticeHandler('Link url is required!')
          } else {
            this.close() // 添加完成后,需要关闭模态框
            this.onSubmit(this.linkName, this.linkUrl) // 调用传过来的 onSubmit 方法
          }
        })
    )
  }
  
  // 省略一小波代码
}

至此,我们的模态框功能已经完成了。还记得我们在 modal.ts 中添加的用于呼起模态框的自定义指令么?现在我们就来用它呼起添加地址的模态框。

点击 Obsidian 界面左侧的命令行图标,在弹出的界面中搜索 Add link(之前在 addCommand 中设置的自定义指令名称):

选择蓝框中的命令后,就会弹出添加地址的模态框了:

直接点击添加按钮,也会在右上角看到提示信息。

删除链接

老规矩,我们还是先来设想下用于删除的模态框需要实现哪些功能:

  1. 首先我们需要添加一个下拉框,用于选择需要删除的地址;
  2. 之后再添加一个按钮,点击后删除所选的地址;
  3. 如果遇到异常场景我们需要给出友好的弹窗提示。

有了添加链接的经验,创建删除链接的模态框就容易多了。在删除链接的模态框中,我们通过 Setting 中提供的 addDropdown 方法添加一个下拉框,用于选择需要删除的地址。此处直接上代码:

export class DeleteLink extends Modal {
  linkName: string
  options: Options

  onDelete: (linkName: string) => void

  constructor (
    app: App,
    options: Options, // 用来接收下拉项
    onDelete: (linkName: string) => void // 删除事件
  ) {
    super(app)
    this.onDelete = onDelete
    this.options = options
    this.linkName = Object.keys(options)[0] || '' // 默认删除第一个
  }

  onOpen (): void {
    const { contentEl } = this

    contentEl.createEl("h1", { text: "Delete Link", cls: "title" })
    
    // 创建下拉框
    new Setting(contentEl).setName("Link name").addDropdown(dp => 
      dp.addOptions(this.options).onChange(value => {
        this.linkName = value
      })
    )
    // 创建删除按钮
    new Setting(contentEl).addButton((btn) =>
      btn
        .setButtonText('Delete')
        .setCta()
        .onClick(() => {
          const { linkName } = this
          if (!linkName) {
            noticeHandler('Link name is required!')
          } else {
            this.close()
            this.onDelete(this.linkName)
          }
        })
    )
  }

  onClose(): void {
    this.contentEl.empty()
  }
}

之后回到 main.ts 文件,我们需要加入 删除模态框的调出指令、获取删除下拉选项的 options 以及添加删除方法

// main.ts

import { Plugin, Editor } from "obsidian"
import { AddLink, DeleteLink } from "./modals"
import { LinkKeeperSettingTab } from './settings'
import { readFile, writeFile } from 'fs/promises'
import { noticeHandler } from './utils'
interface LinkKeeperSettings {
  filepath: string
}

interface Options {
  [key: string]: string
}

const DEFAULT_SETTINGS: Partial<LinkKeeperSettings> = {
  filepath: `${process.env.HOME}/etl.json`
}

export default class LinkKeeperPlugin extends Plugin {
  settings: LinkKeeperSettings

  /**
   * get all links
   * @param cb 
   */
  async getLinks (cb: (data: Options) => void) {
    try {
      const data = await readFile(this.settings.filepath, { encoding: 'utf-8'})
      cb(JSON.parse(data || '{}'))
    } catch (err) {
      noticeHandler(err.message)
    }
  }
  
  /**
   * delete link by name
   * @param name 
   */
  async onDelete (name: string) {
    await this.getLinks(async (data: Options) => {
      delete data[name]
      this.saveLink(data, `Link named ${name} has been deleted!`)
    })
  }

  /**
   * init modal
   * @param type 
   * @param options 
   * @returns 
   */
  initModal (type: string, options?: Options) {
    switch (type) {
      // 省略一小波代码
      
      // 创建删除地址的模态框
      case 'deleteLink':
        return new DeleteLink(this.app, options, this.onDelete.bind(this))

      default: break
    }
  }

  async onload() {
    // 省略一小波代码
    
    // 新增呼出删除模态框的自定义指令
    this.addCommand({
      id: "delete-link",
      name: 'Delete link',
      callback: () => {
        this.getLinks(async (data: Options) => {
          // 创建删除的模态框,并传入 options
          this.initModal('deleteLink', Object.keys(data).reduce((obj, key) => ({
            ...obj,
            [key]: key
          }), {})).open()
        })
      }
    })
  }

  // 省略一小波代码
}

之后,点击 Obsidian 界面左侧的命令行图标,在弹出的界面中搜索 Delete link 并点击,就可以看到删除地址的模态弹框啦:

查看 && 跳转链接

在这个模态框中,我们需要提供以下几种功能:

  1. 提供一个可以按照地址名称进行筛选的搜索框;
  2. 列出已经保存的地址,显示对应的名称以及跳转地址,并且给地址加上链接,以便直接点击跳转

回到 modals.ts ,我们先创建用于罗列地址的模态框:

// modal.ts

export class ListAllLinks extends Modal {
  options: Options

  constructor (
    app: App,
    options: Options // 用于接收地址配置
  ) {
    super(app)
    this.options = options
  }

  onOpen(): void {
    const { contentEl } = this
    // 创建标题
    contentEl.createEl("h1", { text: "All Links", cls: "title" })
  }

  onClose(): void {
    this.contentEl.empty()
  }
}

添加搜索框

这里要用到 Setting 中提供的 addSearch 方法来创建一个搜索框:

// modal.ts

// 省略一小波代码

onOpen(): void {
    const { contentEl } = this

    contentEl.createEl("h1", { text: "All Links", cls: "title" })
    // 创建搜索框
    new Setting(contentEl).setName('Search').addSearch(el => {
      el.setPlaceholder('Input the link name...').onChange(val => {
        // 监听搜索框的 change 事件,在搜索框内容发生变化时重新渲染列表
      })
    })
}

添加地址列表

在上述过程中一直有使用的 Setting 类主要是用来创建表单控件的,而列表内容主要由自定义标签构成,此时我们可以使用 createEl 方法。由于在模态框初始化阶段以及搜索框内容变化阶段都需要渲染列表,因此我们需要将列表渲染的逻辑抽取出来。

// modal.ts

export class ListAllLinks extends Modal {
  options: Options

  constructor (
    app: App,
    options: Options
  ) {
    super(app)
    this.options = options
  }
  
  /**
   * create list item
   * @param container list container
   * @param key link name
   * @param value link url
   * @param isLink determine whether it is a link
   */
  createListItem (container: Element, key: string, value: string, isLink = true) {
    // 向 list 容器中添加地址项
    const box = container.createEl("div", { cls: `list-item ${!isLink ? 'list-item-header' : ''}`})
    // 添加显示地址名称的容器
    box.createEl("div", { text: key })
    // 添加显示 url 的容器
    const linkBox = box.createEl("div")
    // 判断是否是链接
    if (isLink) {
      // 是链接的话添加 a 标签用于跳转
      linkBox.createEl('a', { text: value, href: value})
    } else {
      // 否则直接显示内容
      linkBox.createSpan({ text: value })
    }
  }
  
  // 渲染列表
  renderList (key = ''): Element {
    // 获取所有地址信息
    let options = this.options
    // 根据传过来的 key 筛选需要显示的地址内容
    if (key) {
      options = Object.keys(options).reduce((obj: Options, item) => {
        if (item.includes(key)) obj = { ...obj, [item]: options[item] }
        return obj
      }, {})
    }
    // 创建显示列表的内容容器
    const container = this.contentEl.createEl("div")
    // 添加表头信息
    this.createListItem(container, 'Name', 'Url', false)
    // 创建 list 容器
    const listContainer = container.createEl('div', { cls: 'list-container'})
    const keys = Object.keys(options)
    // 遍历显示列表
    if (keys.length) {
      keys.forEach(key => {
        this.createListItem(listContainer, key, options[key])
      })
    } else {
      // 当没有搜索到相应结果时,显示提示信息
      listContainer.createEl('div', { text: 'No results!', cls: 'list-empty' })
    }

    return container
  }

  onOpen(): void {
    const { contentEl } = this

    contentEl.createEl("h1", { text: "All Links", cls: "title" })

    let contentBox: Element = null

    new Setting(contentEl).setName('Search').addSearch(el => {
      el.setPlaceholder('Input the link name...').onChange(val => {
        // 当搜索内容变化时,清空列表
        contentBox.empty()
        // 重新渲染列表
        contentBox = this.renderList(val)
      })
    })
    // 首次加载时渲染列表
    contentBox = this.renderList()
  }

  onClose(): void {
    this.contentEl.empty()
  }
}

之后,我们要将列表的样式写入 style.css 文件中:

// style.css
/* Sets all the text color to red! */
.title {
  text-align: center;
  font-size: 20px;
}

.list-container {
  max-height: 300px;
  overflow-y: scroll;
}

.list-item-header {
  font-size: 18px;
  font-weight: bold;
}

.list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.list-item div {
  flex: 1;
}

.list-item:not(.list-item-header) div:first-child {
  color: #222;
  font-weight: bold;
}

.list-item div:last-child {
  text-align: right;
}

.list-item:not(.list-item-header) {
  background-color: #c0c0c0;
  padding: 8px;
  border-radius: 4px;
}

.list-empty {
  text-align: center;
  font-size: 16px;
}

最后,我们需要在 main.ts 文件中添加呼起显示列表的自定义指令,并将地址信息传给模态框:

// main.ts

import { Plugin, Editor } from "obsidian"
import { AddLink, DeleteLink, ListAllLinks } from "./modals"
import { LinkKeeperSettingTab } from './settings'
import { readFile, writeFile } from 'fs/promises'
import { noticeHandler } from './utils'
interface LinkKeeperSettings {
  filepath: string
}

interface Options {
  [key: string]: string
}

const DEFAULT_SETTINGS: Partial<LinkKeeperSettings> = {
  filepath: `${process.env.HOME}/etl.json`
}

export default class LinkKeeperPlugin extends Plugin {
  settings: LinkKeeperSettings

  /**
   * get all links
   * @param cb 
   */
  async getLinks (cb: (data: Options) => void) {
    try {
      const data = await readFile(this.settings.filepath, { encoding: 'utf-8'})
      cb(JSON.parse(data || '{}'))
    } catch (err) {
      noticeHandler(err.message)
    }
  }
  
  // 省略一小波代码
  
  /**
   * init modal
   * @param type 
   * @param options 
   * @returns 
   */
  initModal (type: string, options?: Options) {
    switch (type) {
      // 省略一小波代码
      
      case 'listLink':
        return new ListAllLinks(this.app, options)

      default: break
    }
  }

  async onload() {
    // 省略一小波代码
    
    // 添加呼起显示地址列表模态框的指令
    this.addCommand({
      id: 'list-links',
      name: 'List links',
      icon: 'link',
      callback: () => {
        this.getLinks(async (data: Options) => {
          this.initModal('listLink', data).open()
        })
      }
    })
  }
  
  // 省略一小波代码
}

之后,点击 Obsidian 界面左侧的命令行图标,在弹出的界面中搜索 List link 并点击,就可以看到地址列表的模态弹框啦~在搜索框中输入内容,可以看到相应的搜索结果,如果没有符合搜索内容的地址信息,则会显示提示信息:

到这里,我们的插件就基本开发完成啦~~

插件优化

Hotkeys

在上述过程中,我们都是通过自定义指令的方式来呼起模态框,虽然过程并不复杂,但多少还是有些繁琐。Obsidian 给插件的自定义指令提供了设置热键的功能,我们可以通过设置自定义指令的热键来简化这个过程:

添加选中的地址

如果在使用 Obsidian 的过程中,遇到想要保存文档中某个地址的情况,如果要先复制地址,然后再粘贴到模态框中去保存,感觉多少有些麻烦。那么我们是否可以选中想要添加的地址直接进行添加呢?

答案是肯定的!!

此时,我们可以在 addCommand 方法中的 editorCallback 事件中获取到当前编辑器的 editor 对象,并借此获取到选中的地址,之后自动填入模态框中。美滋滋~

接下来,我们需要对先前添加的 Add link 自定义指令以及添加地址的模态框逻辑做些修改。

首先,我们需要将 callback 方法改为 editorCallback 方法,并将选中的内容传给模态框:

// main.ts

// 省略一大波代码

initModal (type: string, options?: Options) {
  switch (type) {
    case 'addLink':
      return new AddLink(this.app, options.link, this.onSubmit.bind(this))
    // 省略一小波代码  
    default: break
  }
}

async onload() {
  // 省略一小波代码
  this.addCommand({
    id: "add-link",
    name: "Add link",
    editorCallback: (editor: Editor) => {
      // 获取选中的地址
      const selection = editor.getSelection()
      this.initModal('addLink', { link: selection }).open()
    }
  })
}

之后,我们需要修改添加地址的模态框代码,接受传入的地址,并将其作为默认值设置到链接地址的文本框中:

// modals.ts

export class AddLink extends Modal {
  linkName: string
  linkUrl: string

  onSubmit: (linkName: string, linkUrl: string) => void

  constructor(
    app: App,
    linkUrl: string, // 接受传入的地址信息
    onSubmit: (linkName: string, linkUrl: string) => void
  ) {
    super(app)
    this.linkName = ''
    this.linkUrl = linkUrl // 将传入的地址信息设置为默认值
    this.onSubmit = onSubmit
  }
  
  // 省略一大波代码
}

改造完成后,此时我们选中想要添加的链接地址,然后通过 热键 的方式呼起模态框,即可看到地址已经被填入其中:

写在最后

行文至此,link-keeper 插件就已经完成啦~有兴趣的小伙伴可以下载使用。如果觉得插件还可的小伙伴们欢迎 star ~

另外,笔者最近在翻译 Obsidian 的插件开发文档,有兴趣的小伙伴可以加入我~ 由于本人水平有限,如果有翻译不到位之处也欢迎大佬们提 issue,以便我及时更正。

注:本文已同步至酱豆腐精的小站