Generate from template

This commit is contained in:
Michael Naumov 2025-03-09 09:42:38 -06:00
commit fe6dccadf6
30 changed files with 11982 additions and 0 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
tab_width = 2
trim_trailing_whitespace = true

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

15
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

71
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,71 @@
name: Bug report
description: Report a bug and help improve the plugin
title: "[BUG] Short description of the bug"
labels: bug
assignees: mnaoumov
body:
- type: markdown
attributes:
value: |
## Bug report
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug. Include any relevant details.
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Provide a step-by-step description.
value: |
1. Go to '...'
2. Click on '...'
3. Notice that '...'
...
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: What actually happened? Include error messages if available.
validations:
required: true
- type: textarea
attributes:
label: Environment Information
description: Environment Information
value: |
- **Plugin Version**: [e.g., 1.0.0]
- **Obsidian Version**: [e.g., v1.3.2]
- **Operating System**: [e.g., Windows 10]
validations:
required: true
- type: textarea
attributes:
label: Attachments
description: Required for bug reproduction
value: |
- Please attach a video showing the bug. It is not mandatory, but might be very helpful to speed up the bug fix
- Please attach a sample vault where the bug can be reproduced. It is not mandatory, but might be very helpful to speed up the bug fix
validations:
required: true
- type: checkboxes
attributes:
label: Confirmations
description: Ensure the following conditions are met
options:
- label: I attached a video showing the bug, or it is not necessary
required: true
- label: I attached a sample vault where the bug can be reproduced, or it is not necessary
required: true
- label: I have tested the bug with the latest version of the plugin
required: true
- label: I have checked GitHub for existing bugs
required: true

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

View file

@ -0,0 +1,61 @@
name: Feature request
description: Request a feature and help improve the plugin
title: "[FR] Short description of the feature"
labels: enhancement
assignees: mnaoumov
body:
- type: markdown
attributes:
value: |
## Feature Request
- type: textarea
attributes:
label: Description
description: A clear and concise description of the feature request. Include any relevant details.
validations:
required: true
- type: textarea
attributes:
label: Details
description: Provide a step-by-step description.
value: |
1. Go to '...'
2. Click on '...'
3. Notice that '...'
...
validations:
required: true
- type: textarea
attributes:
label: Desired Behavior
description: What do you want to happen?
validations:
required: true
- type: textarea
attributes:
label: Current Behavior
description: What actually happens?
validations:
required: true
- type: textarea
attributes:
label: Attachments
description: Required for feature investigation
value: |
- Please attach a video showing the current behavior. It is not mandatory, but might be very helpful to speed up the feature implementation
- Please attach a sample vault where the desired Feature Request could be applied. It is not mandatory, but might be very helpful to speed up the feature implementation
validations:
required: true
- type: checkboxes
attributes:
label: Confirmations
description: Ensure the following conditions are met
options:
- label: I attached a video showing the current behavior, or it is not necessary
required: true
- label: I attached a sample vault where the desired Feature Request could be applied, or it is not necessary
required: true
- label: I have tested the absence of the requested feature with the latest version of the plugin
required: true
- label: I have checked GitHub for existing Feature Requests
required: true

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
dist
.env
/tsconfig.tsbuildinfo

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Michael Naumov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
README.md Normal file
View file

@ -0,0 +1,28 @@
# Backlink Full Path
This is a plugin for [Obsidian](https://obsidian.md/) that Shows the backlink's full path in the backlinks panel.
## Installation
- The plugin is not available in [the official Community Plugins repository](https://obsidian.md/plugins) yet.
- [Beta releases](obsidian://brat?plugin=https://github.com/mnaoumov/obsidian-backlink-full-path) can be installed via [BRAT](https://obsidian.md/plugins?id=obsidian42-brat).
## Debugging
By default, debug messages for this plugin are hidden.
To show them, run the following command:
```js
window.DEBUG.enable('backlink-full-path');
```
For more details, refer to the [documentation](https://github.com/mnaoumov/obsidian-dev-utils?tab=readme-ov-file#debugging).
## Support
<a href="https://www.buymeacoffee.com/mnaoumov" target="_blank">Support this plugin</a>
## License
© [Michael Naumov](https://github.com/mnaoumov/)

17
cspell.json Normal file
View file

@ -0,0 +1,17 @@
{
"version": "0.2",
"ignorePaths": [
"dist",
"node_modules",
"tsconfig.tsbuildinfo"
],
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"dblclick",
"tsbuildinfo"
],
"ignoreWords": [],
"import": [],
"enabled": true
}

1
eslint.config.mjs Normal file
View file

@ -0,0 +1 @@
export { configs as default } from 'obsidian-dev-utils/ScriptUtils/ESLint/eslint.config';

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"author": "mnaoumov",
"authorUrl": "https://github.com/mnaoumov/",
"description": "Shows the backlink&#39;s full path in the backlinks panel.",
"fundingUrl": "https://www.buymeacoffee.com/mnaoumov",
"id": "backlink-full-path",
"isDesktopOnly": false,
"name": "Backlink Full Path",
"version": "0.0.0"
}

10864
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

38
package.json Normal file
View file

@ -0,0 +1,38 @@
{
"name": "backlink-full-path",
"version": "0.0.0",
"description": "Shows the backlink&#39;s full path in the backlinks panel.",
"keywords": [],
"license": "MIT",
"author": "mnaoumov",
"type": "module",
"scripts": {
"build": "obsidian-dev-utils build",
"build:clean": "obsidian-dev-utils build:clean",
"build:compile": "obsidian-dev-utils build:compile",
"build:compile:svelte": "obsidian-dev-utils build:compile:svelte",
"build:compile:typescript": "obsidian-dev-utils build:compile:typescript",
"dev": "obsidian-dev-utils dev",
"format": "obsidian-dev-utils format",
"format:check": "obsidian-dev-utils format:check",
"lint": "obsidian-dev-utils lint",
"lint:fix": "obsidian-dev-utils lint:fix",
"spellcheck": "obsidian-dev-utils spellcheck",
"version": "obsidian-dev-utils version"
},
"devDependencies": {
"@codemirror/language": "^6.10.8",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@tsconfig/strictest": "^2.0.5",
"@types/node": "^22.13.10",
"moment": "^2.30.1",
"obsidian": "^1.8.7",
"obsidian-dev-utils": "^19.8.2",
"obsidian-typings": "^2.35.0",
"svelte": "^5.22.6"
},
"overrides": {
"boolean": "npm:dry-uninstall"
}
}

View file

@ -0,0 +1,251 @@
import type {
Editor,
MarkdownPostProcessorContext,
ObsidianProtocolData,
TAbstractFile,
WorkspaceLeaf
} from 'obsidian';
import {
MarkdownView,
Notice,
PluginSettingTab
} from 'obsidian';
import { convertAsyncToSync } from 'obsidian-dev-utils/Async';
import { getDebugger } from 'obsidian-dev-utils/Debug';
import { alert } from 'obsidian-dev-utils/obsidian/Modals/Alert';
import { confirm } from 'obsidian-dev-utils/obsidian/Modals/Confirm';
import { prompt } from 'obsidian-dev-utils/obsidian/Modals/Prompt';
import { selectItem } from 'obsidian-dev-utils/obsidian/Modals/SelectItem';
import { PluginBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginBase';
import { BacklinkFullPathPluginSettings } from './BacklinkFullPathPluginSettings.ts';
import { BacklinkFullPathPluginSettingsTab } from './BacklinkFullPathPluginSettingsTab.ts';
import { sampleStateField } from './EditorExtensions/SampleStateField.ts';
import { sampleViewPlugin } from './EditorExtensions/SampleViewPlugin.ts';
import { SampleEditorSuggest } from './EditorSuggests/SampleEditorSuggest.ts';
import { SampleModal } from './Modals/SampleModal.ts';
import {
SAMPLE_VIEW_TYPE,
SampleView
} from './Views/SampleView.ts';
export class BacklinkFullPathPlugin extends PluginBase<BacklinkFullPathPluginSettings> {
protected override createPluginSettings(data: unknown): BacklinkFullPathPluginSettings {
return new BacklinkFullPathPluginSettings(data);
}
protected override createPluginSettingsTab(): null | PluginSettingTab {
return new BacklinkFullPathPluginSettingsTab(this);
}
protected override async onLayoutReady(): Promise<void> {
new Notice('This is executed after all plugins are loaded');
await this.openSampleView();
}
protected override onloadComplete(): void {
this.addCommand({
callback: this.runSampleCommand.bind(this),
id: 'sample-command',
name: 'Sample command'
});
this.addCommand({
editorCallback: this.runSampleEditorCommand.bind(this),
id: 'sample-editor-command',
name: 'Sample editor command'
});
this.addCommand({
checkCallback: this.runSampleCommandWithCheck.bind(this),
id: 'sample-command-with-check',
name: 'Sample command with check'
});
this.addRibbonIcon('dice', 'Sample ribbon icon', this.runSampleRibbonIconCommand.bind(this));
this.addStatusBarItem().setText('Sample status bar item');
this.registerDomEvent(document, 'dblclick', this.handleSampleDomEvent.bind(this));
this.registerEditorExtension([sampleViewPlugin, sampleStateField]);
this.registerEditorSuggest(new SampleEditorSuggest(this.app));
this.registerEvent(this.app.vault.on('create', this.handleSampleEvent.bind(this)));
this.registerExtensions(['sample-extension-1', 'sample-extension-2'], SAMPLE_VIEW_TYPE);
this.registerHoverLinkSource(SAMPLE_VIEW_TYPE, {
defaultMod: true,
display: this.manifest.name
});
const INTERVAL_IN_MILLISECONDS = 60_000;
this.registerInterval(window.setInterval(this.handleSampleIntervalTick.bind(this), INTERVAL_IN_MILLISECONDS));
this.registerMarkdownCodeBlockProcessor('sample-code-block-processor', this.handleSampleCodeBlockProcessor.bind(this));
this.registerMarkdownPostProcessor(this.handleSampleMarkdownPostProcessor.bind(this));
this.registerObsidianProtocolHandler('sample-action', this.handleSampleObsidianProtocolHandler.bind(this));
this.registerView(SAMPLE_VIEW_TYPE, (leaf) => new SampleView(leaf));
this.registerModalCommands();
}
private handleSampleCodeBlockProcessor(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): void {
getDebugger('handleSampleCodeBlockProcessor')(source, el, ctx);
el.setText('Sample code block processor');
}
private handleSampleDomEvent(evt: MouseEvent): void {
const tagName = evt.target instanceof HTMLElement ? evt.target.tagName : '';
new Notice(`Sample DOM event: ${tagName}`);
}
private handleSampleEvent(file: TAbstractFile): void {
if (!this.app.workspace.layoutReady) {
return;
}
new Notice(`Sample event: ${file.name}`);
}
private handleSampleIntervalTick(): void {
new Notice('Sample interval tick');
}
private handleSampleMarkdownPostProcessor(el: HTMLElement, ctx: MarkdownPostProcessorContext): void {
getDebugger('handleSampleMarkdownPostProcessor')(el, ctx);
if (el.hasClass('el-h6')) {
el.setText('Sample markdown post processor');
}
}
private handleSampleObsidianProtocolHandler(params: ObsidianProtocolData): void {
new Notice(`Sample obsidian protocol handler: ${params.action}`);
}
private async openSampleView(): Promise<void> {
let leaf: null | WorkspaceLeaf;
const leaves = this.app.workspace.getLeavesOfType(SAMPLE_VIEW_TYPE);
if (leaves.length > 0) {
leaf = leaves[0] ?? null;
} else {
leaf = this.app.workspace.getRightLeaf(false);
await leaf?.setViewState({ active: true, type: SAMPLE_VIEW_TYPE });
}
if (leaf) {
await this.app.workspace.revealLeaf(leaf);
}
}
private registerModalCommands(): void {
this.addCommand({
callback: this.showSampleModal.bind(this),
id: 'show-sample-modal',
name: 'Show Sample Modal'
});
this.addCommand({
callback: convertAsyncToSync(this.showAlert.bind(this)),
id: 'show-alert-modal',
name: 'Show Alert Modal'
});
this.addCommand({
callback: convertAsyncToSync(this.showConfirm.bind(this)),
id: 'show-confirm-modal',
name: 'Show Confirm Modal'
});
this.addCommand({
callback: convertAsyncToSync(this.showPrompt.bind(this)),
id: 'show-prompt-modal',
name: 'Show Prompt Modal'
});
this.addCommand({
callback: convertAsyncToSync(this.showSelectItem.bind(this)),
id: 'show-select-item-modal',
name: 'Show Select Item Modal'
});
}
private runSampleCommand(): void {
new Notice('Sample command');
}
private runSampleCommandWithCheck(checking: boolean): boolean {
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!markdownView) {
return false;
}
if (!checking) {
new Notice('Sample command with check');
}
return true;
}
private runSampleEditorCommand(editor: Editor): void {
editor.replaceSelection('Sample Editor Command');
}
private runSampleRibbonIconCommand(): void {
new Notice('Sample ribbon icon command');
}
private async showAlert(): Promise<void> {
await alert({
app: this.app,
message: 'Sample alert message',
title: 'Sample alert title'
});
}
private async showConfirm(): Promise<void> {
const result = await confirm({
app: this.app,
message: 'Sample confirm message',
title: 'Sample confirm title'
});
new Notice(`Sample confirm result: ${result.toString()}`);
}
private async showPrompt(): Promise<void> {
await prompt({
app: this.app,
defaultValue: 'Sample prompt default value',
placeholder: 'Sample prompt placeholder',
title: 'Sample prompt title',
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
valueValidator: (value): string | void => {
const MIN_LENGTH = 30;
if (value.length < MIN_LENGTH) {
return `Value must be at least ${MIN_LENGTH.toString()} characters long`;
}
}
});
}
private showSampleModal(): void {
new SampleModal(this.app).open();
}
private async showSelectItem(): Promise<void> {
await selectItem({
app: this.app,
items: ['Item 1', 'Item 2', 'Item 3'],
itemTextFunc: (item) => item,
placeholder: 'Sample select item placeholder'
});
}
}

View file

@ -0,0 +1,45 @@
import type { Duration } from 'moment';
import type { IsoMonth } from 'obsidian-dev-utils/obsidian/Components/MonthComponent';
import type { IsoWeek } from 'obsidian-dev-utils/obsidian/Components/WeekComponent';
import { duration } from 'moment';
import { PluginSettingsBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginSettingsBase';
export class BacklinkFullPathPluginSettings extends PluginSettingsBase {
/* eslint-disable no-magic-numbers */
public colorSetting = '#123456';
public dateSetting = new Date();
public dateTimeSetting = new Date();
public dropdownSetting = 'Value2';
public emailSetting = 'defaultEmail@example.com';
public momentFormatSetting = 'YYYY-MM-DD';
public monthSetting: IsoMonth = {
month: 2,
year: 2025
};
public multipleDropdownSetting: string[] = ['Value2', 'Value5'];
public multipleEmailSetting: string[] = ['defaultEmail@example.com', 'defaultEmail2@example.com'];
public multipleTextSetting: string[] = ['defaultText1', 'defaultText2', 'defaultText3'];
public numberSetting = 123;
public progressBarSetting = 50;
public searchSetting = 'defaultSearch';
public sliderSetting = 50;
public textAreaSetting = 'defaultTextArea';
public textSetting = 'defaultText';
public timeSetting: Duration = duration({ hours: 12, minutes: 34 });
public toggleSetting = true;
public urlSetting = 'https://example.com';
public weekSetting: IsoWeek = {
weekNumber: 6,
year: 2025
};
/* eslint-enable no-magic-numbers */
public constructor(data: unknown) {
super();
this.init(data);
}
}

View file

@ -0,0 +1,222 @@
import { PluginSettingsTabBase } from 'obsidian-dev-utils/obsidian/Plugin/PluginSettingsTabBase';
import { SettingEx } from 'obsidian-dev-utils/obsidian/SettingEx';
import type { BacklinkFullPathPlugin } from './BacklinkFullPathPlugin.ts';
export class BacklinkFullPathPluginSettingsTab extends PluginSettingsTabBase<BacklinkFullPathPlugin> {
public override display(): void {
this.containerEl.empty();
new SettingEx(this.containerEl)
.setName('Button Setting Name')
.setDesc('Button Setting Description.')
.addButton((button) => {
button.setButtonText('Button Text')
.onClick(() => {
new Notice('Button clicked');
});
});
new SettingEx(this.containerEl)
.setName('Color Setting Name')
.setDesc('Color Setting Description.')
.addColorPicker((color) => {
this.bind(color, 'colorSetting');
});
new SettingEx(this.containerEl)
.setName('Date Setting Name')
.setDesc('Date Setting Description.')
.addDate((date) => {
this.bind(date, 'dateSetting');
});
new SettingEx(this.containerEl)
.setName('DateTime Setting Name')
.setDesc('DateTime Setting Description.')
.addDateTime((dateTime) => {
this.bind(dateTime, 'dateTimeSetting');
});
new SettingEx(this.containerEl)
.setName('Dropdown Setting Name')
.setDesc('Dropdown Setting Description.')
.addDropdown((dropdown) => {
dropdown.addOptions({
Value1: 'Display 1',
Value2: 'Display 2',
Value3: 'Display 3'
});
this.bind(dropdown, 'dropdownSetting');
});
new SettingEx(this.containerEl)
.setName('Email Setting Name')
.setDesc('Email Setting Description.')
.addEmail((email) => {
this.bind(email, 'emailSetting');
});
new SettingEx(this.containerEl)
.setName('Extra Button Setting Name')
.setDesc('Extra Button Setting Description.')
.addExtraButton((extraButton) => {
extraButton
.onClick(() => {
new Notice('Extra Button clicked');
});
});
new SettingEx(this.containerEl)
.setName('File Setting Name')
.setDesc('File Setting Description.')
.addFile((file) => {
file.onChange((value) => {
new Notice(`File selected: ${value?.name ?? '(None)'}`);
});
});
new SettingEx(this.containerEl)
.setName('Moment Format Setting Name')
.setDesc('Moment Format Setting Description.')
.addMomentFormat((momentFormat) => {
this.bind(momentFormat, 'momentFormatSetting');
});
new SettingEx(this.containerEl)
.setName('Month Setting Name')
.setDesc('Month Setting Description.')
.addMonth((month) => {
this.bind(month, 'monthSetting');
});
new SettingEx(this.containerEl)
.setName('Multiple Dropdown Setting Name')
.setDesc('Multiple Dropdown Setting Description.')
.addMultipleDropdown((multipleDropdown) => {
multipleDropdown.addOptions({
Value1: 'Display 1',
Value2: 'Display 2',
Value3: 'Display 3',
Value4: 'Display 4',
Value5: 'Display 5'
});
this.bind(multipleDropdown, 'multipleDropdownSetting');
});
new SettingEx(this.containerEl)
.setName('Multiple Email Setting Name')
.setDesc('Multiple Email Setting Description.')
.addMultipleEmail((multipleEmail) => {
this.bind(multipleEmail, 'multipleEmailSetting');
});
new SettingEx(this.containerEl)
.setName('Multiple File Setting Name')
.setDesc('Multiple File Setting Description.')
.addMultipleFile((multipleFile) => {
multipleFile.onChange((value) => {
const fileNames = value.map((file) => file.name);
new Notice(`Files selected: ${fileNames.join(', ')}`);
});
});
new SettingEx(this.containerEl)
.setName('Multiple Text Setting Name')
.setDesc('Multiple Text Setting Description.')
.addMultipleText((multipleText) => {
this.bind(multipleText, 'multipleTextSetting');
});
new SettingEx(this.containerEl)
.setName('Number Setting Name')
.setDesc('Number Setting Description.')
.addNumber((number) => {
this.bind(number, 'numberSetting');
});
new SettingEx(this.containerEl)
.setName('ProgressBar Setting Name')
.setDesc('ProgressBar Setting Description.')
.addProgressBar((progressBar) => {
progressBar.setValue(this.plugin.settings.progressBarSetting);
});
new SettingEx(this.containerEl)
.setName('Search Setting Name')
.setDesc('Search Setting Description.')
.addSearch((search) => {
this.bind(search, 'searchSetting');
});
new SettingEx(this.containerEl)
.setName('Slider Setting Name')
.setDesc('Slider Setting Description.')
.addSlider((slider) => {
this.bind(slider, 'sliderSetting');
});
new SettingEx(this.containerEl)
.setName('Text Setting Name')
.setDesc('Text Setting Description.')
.addText((text) => {
this.bind(text, 'textSetting');
});
new SettingEx(this.containerEl)
.setName('Text Area Setting Name')
.setDesc('Text Area Setting Description.')
.addTextArea((textArea) => {
this.bind(textArea, 'textAreaSetting');
});
new SettingEx(this.containerEl)
.setName('Time Setting Name')
.setDesc('Time Setting Description.')
.addTime((time) => {
this.bind(time, 'timeSetting');
});
new SettingEx(this.containerEl)
.setName('Toggle Setting Name')
.setDesc('Toggle Setting Description.')
.addToggle((toggle) => {
this.bind(toggle, 'toggleSetting');
});
new SettingEx(this.containerEl)
.setName('Url Setting Name')
.setDesc('Url Setting Description.')
.addUrl((url) => {
this.bind(url, 'urlSetting');
});
new SettingEx(this.containerEl)
.setName('Week Setting Name')
.setDesc('Week Setting Description.')
.addWeek((week) => {
this.bind(week, 'weekSetting');
});
new SettingEx(this.containerEl)
.setName('Advanced Text Setting Name')
.setDesc('Advanced Text Setting Description.')
.addText((text) => {
this.bind(text, 'textSetting', {
componentToPluginSettingsValueConverter: (uiValue: string) => uiValue.replace(' (converted)', ''),
onChanged: () => {
new Notice('Advanced Text Setting changed');
},
pluginSettingsToComponentValueConverter: (pluginSettingsValue: string) => `${pluginSettingsValue} (converted)`,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
valueValidator(uiValue): string | void {
if (uiValue === '') {
return 'Value must be non-empty';
}
}
})
.setPlaceholder('Enter a value');
});
}
}

View file

@ -0,0 +1,51 @@
import type { Extension } from '@codemirror/state';
import type { DecorationSet } from '@codemirror/view';
import type { StateFieldSpec } from 'obsidian-dev-utils/codemirror/StateFieldSpec';
import { syntaxTree } from '@codemirror/language';
import {
RangeSetBuilder,
StateField,
Transaction
} from '@codemirror/state';
import {
Decoration,
EditorView
} from '@codemirror/view';
import { SampleWidget } from './SampleWidget.ts';
class SampleStateField implements StateFieldSpec<DecorationSet> {
public create(): DecorationSet {
return Decoration.none;
}
public provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
}
public update(_oldState: DecorationSet, transaction: Transaction): DecorationSet {
const OFFSET = 2;
const builder = new RangeSetBuilder<Decoration>();
syntaxTree(transaction.state).iterate({
enter(node) {
if (node.type.name.startsWith('list')) {
const listCharFrom = node.from - OFFSET;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new SampleWidget()
})
);
}
}
});
return builder.finish();
}
}
export const sampleStateField = StateField.define<DecorationSet>(new SampleStateField());

View file

@ -0,0 +1,66 @@
import type {
DecorationSet,
PluginSpec,
PluginValue
} from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
import { RangeSetBuilder } from '@codemirror/state';
import {
Decoration,
EditorView,
ViewPlugin,
ViewUpdate
} from '@codemirror/view';
import { SampleWidget } from './SampleWidget.ts';
class SampleViewPlugin implements PluginValue {
public decorations: DecorationSet;
public constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
public buildDecorations(view: EditorView): DecorationSet {
const OFFSET = 2;
const builder = new RangeSetBuilder<Decoration>();
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
enter(node) {
if (node.type.name.startsWith('list')) {
const listCharFrom = node.from - OFFSET;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new SampleWidget()
})
);
}
},
from,
to
});
}
return builder.finish();
}
public update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
}
const pluginSpec: PluginSpec<SampleViewPlugin> = {
decorations: (value: SampleViewPlugin) => value.decorations
};
export const sampleViewPlugin = ViewPlugin.fromClass(
SampleViewPlugin,
pluginSpec
);

View file

@ -0,0 +1,7 @@
import { WidgetType } from '@codemirror/view';
export class SampleWidget extends WidgetType {
public toDOM(): HTMLElement {
return createEl('span', { text: '👉' });
}
}

View file

@ -0,0 +1,44 @@
import type {
Editor,
EditorPosition,
EditorSuggestContext,
EditorSuggestTriggerInfo,
TFile
} from 'obsidian';
import { EditorSuggest } from 'obsidian';
export class SampleEditorSuggest extends EditorSuggest<string> {
private cursor: EditorPosition | null = null;
private editor: Editor | null = null;
public override getSuggestions(context: EditorSuggestContext): Promise<string[]> | string[] {
return [`${context.query} 1`, `${context.query} 2`, `${context.query} 3`];
}
public override onTrigger(cursor: EditorPosition, editor: Editor, file: null | TFile): EditorSuggestTriggerInfo | null {
this.cursor = cursor;
this.editor = editor;
const line = editor.getLine(cursor.line);
if (line !== 'Sample') {
return null;
}
return {
end: cursor,
query: `Query ${file?.name ?? ''}`,
start: cursor
};
}
public override renderSuggestion(value: string, el: HTMLElement): void {
el.createEl('strong', { text: value });
}
public override selectSuggestion(value: string, evt: KeyboardEvent | MouseEvent): void {
this.close();
if (this.editor && this.cursor) {
this.editor.replaceRange(`Transformed ${value} ${evt.type}`, this.cursor);
}
}
}

View file

@ -0,0 +1,7 @@
import { Modal } from 'obsidian';
export class SampleModal extends Modal {
public override onOpen(): void {
this.contentEl.setText('Sample Modal');
}
}

View file

@ -0,0 +1,7 @@
export interface SampleComponentExports {
increment(): void;
}
export interface SampleComponentProps {
startCount: number;
}

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { SampleComponentProps } from './SampleComponent.d.ts';
let { startCount }: SampleComponentProps = $props();
let count = $state(startCount);
export function increment() {
count += 1;
}
</script>
<div class="number">
<span>My number is {count}!</span>
</div>
<style>
.number {
color: red;
}
</style>

47
src/Views/SampleView.ts Normal file
View file

@ -0,0 +1,47 @@
import { ItemView } from 'obsidian';
import {
mount,
unmount
} from 'svelte';
import type {
SampleComponentExports,
SampleComponentProps
} from '../SvelteComponents/SampleComponent.d.ts';
import SampleComponent from '../SvelteComponents/SampleComponent.svelte';
export const SAMPLE_VIEW_TYPE = 'backlink-full-path-sample-view';
export class SampleView extends ItemView {
private sampleComponent: null | SampleComponentExports = null;
public override getDisplayText(): string {
return 'Sample view';
}
public override getViewType(): string {
return SAMPLE_VIEW_TYPE;
}
public override async onClose(): Promise<void> {
if (this.sampleComponent) {
await unmount(this.sampleComponent);
}
}
public override async onOpen(): Promise<void> {
const START_COUNT = 10;
const props: SampleComponentProps = {
startCount: START_COUNT
};
this.sampleComponent = mount(SampleComponent, {
props,
target: this.contentEl
}) as SampleComponentExports;
this.sampleComponent.increment();
await Promise.resolve();
}
}

5
src/main.ts Normal file
View file

@ -0,0 +1,5 @@
import './styles/main.scss';
import { BacklinkFullPathPlugin } from './BacklinkFullPathPlugin.ts';
// eslint-disable-next-line import-x/no-default-export
export default BacklinkFullPathPlugin;

9
src/styles/main.scss Normal file
View file

@ -0,0 +1,9 @@
/**
* You can remove this file if you don't need any styles
* See default library styles: https://github.com/mnaoumov/obsidian-dev-utils/blob/main/src/styles/main.scss
* See available library css classes: https://github.com/mnaoumov/obsidian-dev-utils/blob/main/src/CssClass.ts
*/
.backlink-full-path.obsidian-dev-utils {
/* Override library styles */
}

33
tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"inlineSourceMap": true,
"inlineSources": true,
"lib": [
"DOM",
"ES2024"
],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"skipLibCheck": false,
"target": "ES2024",
"types": [
"node",
"obsidian-typings",
"svelte"
],
"verbatimModuleSyntax": true
},
"include": [
"./src/**/*.svelte",
"./src/**/*.ts",
"./scripts/**/*.ts"
]
}

1
versions.json Normal file
View file

@ -0,0 +1 @@
{}