├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── contribution-request.md
│ └── feature_request.md
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── manifest.json
├── package.json
├── src
├── LinkTreeView.tsx
├── components
│ ├── Link.tsx
│ ├── Note.tsx
│ ├── Search.tsx
│ └── View.tsx
├── main.ts
├── services
│ ├── ObsidianAPI.ts
│ └── store.ts
├── styles.css
└── types
│ └── index.d.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: joshuatazrein
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **If possible, paste error trace here:**
14 |
15 | **To Reproduce**
16 | Steps to reproduce the behavior:
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | **Expected behavior**
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Screenshots**
26 | If applicable, add screenshots to help explain your problem.
27 |
28 | **Obsidian Version:**
29 | - [ ] Mobile
30 | - [ ] Desktop
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/contribution-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Contribution request
3 | about: If you would like to add to this plugin, please submit a contribution request describing
4 | your idea.
5 | title: ''
6 | labels: help wanted
7 | assignees: ''
8 |
9 | ---
10 |
11 | Your name & background:
12 | Describe your idea:
13 | Why is this important for Link Tree?
14 | How long will it take to implement this feature? (ex: 1 week, 1 month)
15 | Are there any other plugins that have this feature?
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: joshuatazrein
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm
2 | node_modules
3 | dist
4 |
5 | # Don't include the compiled main.js file in the repo.
6 | # They should be uploaded to GitHub releases instead.
7 | main.js
8 |
9 | # obsidian
10 | data.json
11 |
12 | # Exclude macOS Finder (System Explorer) View States
13 | .DS_Store
14 | **/*/.DS_Store
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # Planned
3 | - [ ] Filter out (negative search)
4 | - [ ] add [aliases](https://github.com/joshuatazrein/obsidian-link-tree/issues/2#event-10732015435)
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | joshuatreinier@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | If you would like to contribute to the plugin, post an issue describing your contribution or idea and tag with "contribution." The developer (joshuatazrein) will respond to discuss it.
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Joshua Tazman Reinier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Link Tree presents a view of the current note's links and backlinks in a recursively expandable list, similar to outliners like WorkFlowy or Dynalist.
2 |
3 |
4 |
5 | # Features
6 | - Click on the bullet point to expand or collapse a node, showing/hiding its forward and backlinks (circular links are filtered out of the hierarchy).
7 | - Toggle showing forward and back links by clicking the arrow buttons in the top-right
8 | - Filter results by pathname by typing in the search box
9 | - Click the dots button to show note content: a single line of text, full text, or nothing (text is editable and lines will save to the note)
10 |
11 | # Note
12 | - This plugin requires the Dataview plugin to function, as it uses the Dataview API to extract links and backlinks. Please install it before using this plugin.
13 |
14 | # Credit
15 | - Many thanks to Dataview for making it easy and synchronous to parse links and backlinks!
16 | - This plugin uses libraries including React, react-transition, react-contenteditable, and Tailwind CSS.
17 | - WorkFlowy's design inspired the look for the plugin.
18 |
19 | If you appreciate this plugin, I would love your support for further development!
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | The current version is supported.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | If you notice a vulnerability, please send an email to me at joshuatreinier@gmail.com.
10 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "link-tree",
3 | "name": "Link Tree",
4 | "version": "1.0.0",
5 | "minAppVersion": "0.15.0",
6 | "description": "View file links and backlinks as a recursively expandable, filterable list with editable text, combining the structure of outliners like Dynalist & WorkFlowy with the flexibility of Obsidian.",
7 | "author": "Joshua Tazman Reinier",
8 | "authorUrl": "https://joshuareinier.com",
9 | "fundingUrl": "https://bmc.link/joshuatreinier",
10 | "isDesktopOnly": false
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-foldable-list",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "cd ../../ && npm run dev:fl"
8 | },
9 | "keywords": [],
10 | "author": "Joshua Reinier",
11 | "license": "ISC"
12 | }
13 |
--------------------------------------------------------------------------------
/src/LinkTreeView.tsx:
--------------------------------------------------------------------------------
1 | import { App, ItemView, WorkspaceLeaf } from 'obsidian'
2 | import ObsidianAPI from './services/ObsidianAPI'
3 | import * as React from 'react'
4 | import { Root, createRoot } from 'react-dom/client'
5 | import { StrictMode } from 'react'
6 | import View from './components/View'
7 | import { getStore } from './services/store'
8 |
9 | export default class LinkTreeView extends ItemView {
10 | obsidianAPI: ObsidianAPI
11 | root: Root
12 |
13 | constructor(leaf: WorkspaceLeaf) {
14 | super(leaf)
15 | this.icon = 'list-tree'
16 | this.navigation = false
17 | this.obsidianAPI = new ObsidianAPI(this.app)
18 | getStore('setState')({ obsidianAPI: this.obsidianAPI })
19 | }
20 |
21 | getDisplayText(): string {
22 | return 'Link Tree'
23 | }
24 |
25 | getViewType(): string {
26 | return 'link-tree'
27 | }
28 |
29 | async onOpen() {
30 | this.root = createRoot(this.containerEl.children[1])
31 |
32 | this.root.render()
33 | }
34 |
35 | async onClose() {
36 | this.root.unmount()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState, useMemo } from 'react'
2 | import { getObsidianAPI, getStore, useStore } from '../services/store'
3 | import invariant from 'tiny-invariant'
4 | import { Transition } from 'react-transition-group'
5 | import {
6 | TransitionActions,
7 | TransitionStatus,
8 | } from 'react-transition-group/Transition'
9 | import _ from 'lodash'
10 | import { Link } from '../services/ObsidianAPI'
11 | import $ from 'jquery'
12 | import Logo from 'packages/obsidian-components/Logo'
13 | import Button from 'packages/obsidian-components/Button'
14 | import Note from './Note'
15 |
16 | export default function Link({
17 | link,
18 | forward,
19 | back,
20 | parents,
21 | backlinkTo,
22 | }: {
23 | link: string
24 | forward: boolean
25 | back: boolean
26 | parents: Set
27 | backlinkTo: string
28 | }) {
29 | const showForward = useStore((state) => state.showForward)
30 | const showBack = useStore((state) => state.showBack)
31 |
32 | const thisLink = useStore((state) => state.links[link] as Link | undefined)
33 |
34 | const [collapsed, setCollapsed] = useState(true)
35 | const [loadedChildren, setLoadedChildren] = useState(false)
36 |
37 | const shownChildren = thisLink?.children.filter(
38 | ({ forward, back, link: childLink }) =>
39 | ((forward && showForward) || (back && showBack)) &&
40 | childLink !== link &&
41 | !parents.has(childLink)
42 | )
43 |
44 | const nodeRef = useRef(null)
45 |
46 | const matchedSearch = useStore(
47 | (state) =>
48 | !state.search || new RegExp(_.escapeRegExp(state.search), 'i').test(link)
49 | )
50 |
51 | const childrenHeight = !nodeRef.current
52 | ? thisLink?.children.length ?? 0 * 20
53 | : $(nodeRef.current)
54 | .children()
55 | .toArray()
56 | .reduce((total, el) => total + el.getBoundingClientRect().height, 0)
57 | const transitionStyles: Partial<
58 | Record
59 | > = {
60 | entering: {
61 | maxHeight: !thisLink?.children ? 0 : childrenHeight,
62 | },
63 | entered: { maxHeight: '' },
64 | exiting: {
65 | maxHeight: !thisLink?.children ? 0 : childrenHeight,
66 | },
67 | exited: { maxHeight: 0 },
68 | }
69 |
70 | const newParents = new Set(parents)
71 | newParents.add(link)
72 | if (thisLink)
73 | thisLink.children
74 | .map((child) => child.link)
75 | .forEach((child) => newParents.add(child))
76 |
77 | const [showingNotes, setShowingNotes] = useState<'one' | 'all' | 'none'>(
78 | 'none'
79 | )
80 |
81 | const shownNotes = useMemo(() => {
82 | const linkTest = new RegExp(
83 | `\\[\\[(.*?\\/)?${_.escapeRegExp(
84 | backlinkTo.slice(
85 | backlinkTo.includes('/') ? backlinkTo.lastIndexOf('/') + 1 : 0
86 | )
87 | )}(#.*?)?(\\|.*?)?\\]\\]`
88 | )
89 | return showingNotes === 'none'
90 | ? []
91 | : showingNotes === 'all'
92 | ? thisLink?.notes
93 | : !back
94 | ? thisLink?.notes?.slice(0, 1)
95 | : thisLink?.notes?.filter((note) => {
96 | return linkTest.test(note.text)
97 | })
98 | }, [backlinkTo, thisLink?.notes, showingNotes])
99 |
100 | return (
101 |