├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── README.md ├── assets └── Collapse-Node.gif ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── src ├── ControlHeader.ts ├── index.ts ├── patchUtils.ts ├── types │ ├── canvas.d.ts │ ├── custom.d.ts │ ├── event.d.ts │ └── obsidian.d.ts └── utils.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: [ "bug" ] 5 | body: 6 | - type: textarea 7 | id: bug-description 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain your problem. 18 | - type: textarea 19 | id: reproduction-steps 20 | attributes: 21 | label: To Reproduce 22 | description: Steps to reproduce the problem 23 | placeholder: | 24 | For example: 25 | 1. Go to '...' 26 | 2. Click on '...' 27 | 3. Scroll down to '...' 28 | - type: input 29 | id: obsi-version 30 | attributes: 31 | label: Obsidian Version 32 | description: You can find the version in the *About* Tab of the settings. 33 | placeholder: 0.13.19 34 | validations: 35 | required: true 36 | - type: checkboxes 37 | id: checklist 38 | attributes: 39 | label: Checklist 40 | options: 41 | - label: I updated to the latest version of the plugin. 42 | required: true 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: [ "feature request" ] 5 | body: 6 | - type: textarea 7 | id: feature-requested 8 | attributes: 9 | label: Feature Requested 10 | description: A clear and concise description of the feature. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain the request. 18 | - type: checkboxes 19 | id: checklist 20 | attributes: 21 | label: Checklist 22 | options: 23 | - label: The feature would be useful to more users than just me. 24 | required: true 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | 7 | env: 8 | PLUGIN_NAME: obsidian-collapse-node 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - name: Build 20 | id: build 21 | run: | 22 | npm install 23 | npm run build 24 | mkdir ${{ env.PLUGIN_NAME }} 25 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 26 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 27 | ls 28 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 29 | - name: Upload zip file 30 | id: upload-zip 31 | uses: actions/upload-release-asset@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | upload_url: ${{ github.event.release.upload_url }} 36 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 37 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 38 | asset_content_type: application/zip 39 | 40 | - name: Upload main.js 41 | id: upload-main 42 | uses: actions/upload-release-asset@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | upload_url: ${{ github.event.release.upload_url }} 47 | asset_path: ./main.js 48 | asset_name: main.js 49 | asset_content_type: text/javascript 50 | 51 | - name: Upload manifest.json 52 | id: upload-manifest 53 | uses: actions/upload-release-asset@v1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | upload_url: ${{ github.event.release.upload_url }} 58 | asset_path: ./manifest.json 59 | asset_name: manifest.json 60 | asset_content_type: application/json 61 | 62 | - name: Upload styles.css 63 | id: upload-css 64 | uses: actions/upload-release-asset@v1 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | with: 68 | upload_url: ${{ github.event.release.upload_url }} 69 | asset_path: ./styles.css 70 | asset_name: styles.css 71 | asset_content_type: text/css 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Collapse Node 2 | 3 | A plugin for [Obsidian](https://obsidian.md) that helps you collapse node in canvas. 4 | 5 | ![Collapse-Node](./assets/Collapse-Node.gif) 6 | 7 | ## Usage 8 | 9 | - Commands 10 | - `Fold All Nodes` - Collapses all nodes in the workspace 11 | - `Expand All Nodes` - Expands all nodes in the workspace 12 | - `Fold Selected Nodes` - Collapses only the selected nodes 13 | - `Expand Selected Nodes` - Expands only the selected nodes 14 | - Context menu in canvas 15 | - `Selection Menu` - Shows the context menu for selected nodes 16 | - `Node Menu` - Shows the context menu for a specific node or nodes 17 | - `Set Node Alias` - Set a custom alias for the node (shown in collapsed state) 18 | - `Set Node Thumbnail` - Set a custom thumbnail image for the node (shown in collapsed state) 19 | - `Remove Node Customizations` - Remove alias and thumbnail from the node 20 | - Direct Click on the Header 21 | - `Click` - Collapse or expand the node 22 | - Thumbnails and Aliases in Collapsed State 23 | - Enable in settings to show thumbnails and/or aliases when nodes are collapsed 24 | - For thumbnails: Add `thumbnail: path/to/image.jpg` or `thumbnail: https://example.com/image.jpg` to node metadata, or use the context menu 25 | - For aliases: Add `alias: Your Alias Text` to node metadata, use frontmatter aliases for file nodes, or use the context menu 26 | 27 | ## Installation 28 | 29 | - Not ready for market yet 30 | - Can be installed via the [Brat](https://github.com/TfTHacker/obsidian42-brat) plugin 31 | - Manual installation 32 | 33 | 1. Find the release page on this github page and click 34 | 2. Download the latest release zip file 35 | 3. Unzip it, copy the unzipped folder to the obsidian plugin folder, make sure there are main.js and manifest.json files 36 | in the folder 37 | 4. Restart obsidian (do not restart also, you have to refresh plugin list), in the settings interface to enable the 38 | plugin 39 | 5. Done! 40 | 41 | ## Support 42 | 43 | If you are enjoying this plugin then please support my work and enthusiasm by buying me a coffee 44 | on [https://www.buymeacoffee.com/boninall](https://www.buymeacoffee.com/boninall). 45 | . 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/Collapse-Node.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quorafind/Obsidian-Collapse-Node/60a78514cd1481e35afafbc25beebe7e1d04d5cc/assets/Collapse-Node.gif -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/index.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins, 35 | ], 36 | format: "cjs", 37 | watch: !prod, 38 | target: "es2018", 39 | logLevel: "info", 40 | sourcemap: prod ? false : "inline", 41 | treeShaking: true, 42 | outfile: "main.js", 43 | }) 44 | .catch(() => process.exit(1)); 45 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "collapse-node", 3 | "name": "Collapse Node", 4 | "version": "2.0.0", 5 | "minAppVersion": "1.1.0", 6 | "description": "Collapse node in canvas.", 7 | "author": "Boninall", 8 | "authorUrl": "https://github.com/Quorafind", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/boninall", 11 | "爱发电": "https://afdian.net/a/boninall", 12 | "支付宝": "https://cdn.jsdelivr.net/gh/Quorafind/.github@main/IMAGE/%E6%94%AF%E4%BB%98%E5%AE%9D%E4%BB%98%E6%AC%BE%E7%A0%81.jpg" 13 | }, 14 | "isDesktopOnly": false 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collapse-node", 3 | "version": "2.0.0", 4 | "description": "A plugin for collapse node in canvas", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.14.47", 20 | "obsidian": "^1.8.7", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4" 23 | }, 24 | "dependencies": { 25 | "@codemirror/state": "^6.4.1", 26 | "@codemirror/view": "^6.28.1", 27 | "monkey-around": "^2.3.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@codemirror/state': 12 | specifier: ^6.4.1 13 | version: 6.4.1 14 | '@codemirror/view': 15 | specifier: ^6.28.1 16 | version: 6.28.1 17 | monkey-around: 18 | specifier: ^2.3.0 19 | version: 2.3.0 20 | devDependencies: 21 | '@types/node': 22 | specifier: ^16.11.6 23 | version: 16.18.46 24 | '@typescript-eslint/eslint-plugin': 25 | specifier: 5.29.0 26 | version: 5.29.0(@typescript-eslint/parser@5.29.0(eslint@8.48.0)(typescript@4.7.4))(eslint@8.48.0)(typescript@4.7.4) 27 | '@typescript-eslint/parser': 28 | specifier: 5.29.0 29 | version: 5.29.0(eslint@8.48.0)(typescript@4.7.4) 30 | builtin-modules: 31 | specifier: 3.3.0 32 | version: 3.3.0 33 | esbuild: 34 | specifier: 0.14.47 35 | version: 0.14.47 36 | obsidian: 37 | specifier: ^1.8.7 38 | version: 1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.28.1) 39 | tslib: 40 | specifier: 2.4.0 41 | version: 2.4.0 42 | typescript: 43 | specifier: 4.7.4 44 | version: 4.7.4 45 | 46 | packages: 47 | 48 | '@aashutoshrathi/word-wrap@1.2.6': 49 | resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 50 | engines: {node: '>=0.10.0'} 51 | 52 | '@codemirror/state@6.4.1': 53 | resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} 54 | 55 | '@codemirror/view@6.28.1': 56 | resolution: {integrity: sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==} 57 | 58 | '@eslint-community/eslint-utils@4.4.0': 59 | resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} 60 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 61 | peerDependencies: 62 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 63 | 64 | '@eslint-community/regexpp@4.8.0': 65 | resolution: {integrity: sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==} 66 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 67 | 68 | '@eslint/eslintrc@2.1.2': 69 | resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} 70 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 71 | 72 | '@eslint/js@8.48.0': 73 | resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} 74 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 75 | 76 | '@humanwhocodes/config-array@0.11.10': 77 | resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} 78 | engines: {node: '>=10.10.0'} 79 | deprecated: Use @eslint/config-array instead 80 | 81 | '@humanwhocodes/module-importer@1.0.1': 82 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 83 | engines: {node: '>=12.22'} 84 | 85 | '@humanwhocodes/object-schema@1.2.1': 86 | resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} 87 | deprecated: Use @eslint/object-schema instead 88 | 89 | '@nodelib/fs.scandir@2.1.5': 90 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 91 | engines: {node: '>= 8'} 92 | 93 | '@nodelib/fs.stat@2.0.5': 94 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 95 | engines: {node: '>= 8'} 96 | 97 | '@nodelib/fs.walk@1.2.8': 98 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 99 | engines: {node: '>= 8'} 100 | 101 | '@types/codemirror@5.60.8': 102 | resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} 103 | 104 | '@types/estree@1.0.1': 105 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 106 | 107 | '@types/json-schema@7.0.12': 108 | resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} 109 | 110 | '@types/node@16.18.46': 111 | resolution: {integrity: sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==} 112 | 113 | '@types/tern@0.23.4': 114 | resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==} 115 | 116 | '@typescript-eslint/eslint-plugin@5.29.0': 117 | resolution: {integrity: sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==} 118 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 119 | peerDependencies: 120 | '@typescript-eslint/parser': ^5.0.0 121 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 122 | typescript: '*' 123 | peerDependenciesMeta: 124 | typescript: 125 | optional: true 126 | 127 | '@typescript-eslint/parser@5.29.0': 128 | resolution: {integrity: sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==} 129 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 130 | peerDependencies: 131 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 132 | typescript: '*' 133 | peerDependenciesMeta: 134 | typescript: 135 | optional: true 136 | 137 | '@typescript-eslint/scope-manager@5.29.0': 138 | resolution: {integrity: sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==} 139 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 140 | 141 | '@typescript-eslint/type-utils@5.29.0': 142 | resolution: {integrity: sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==} 143 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 144 | peerDependencies: 145 | eslint: '*' 146 | typescript: '*' 147 | peerDependenciesMeta: 148 | typescript: 149 | optional: true 150 | 151 | '@typescript-eslint/types@5.29.0': 152 | resolution: {integrity: sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==} 153 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 154 | 155 | '@typescript-eslint/typescript-estree@5.29.0': 156 | resolution: {integrity: sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==} 157 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 158 | peerDependencies: 159 | typescript: '*' 160 | peerDependenciesMeta: 161 | typescript: 162 | optional: true 163 | 164 | '@typescript-eslint/utils@5.29.0': 165 | resolution: {integrity: sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==} 166 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 167 | peerDependencies: 168 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 169 | 170 | '@typescript-eslint/visitor-keys@5.29.0': 171 | resolution: {integrity: sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==} 172 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 173 | 174 | acorn-jsx@5.3.2: 175 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 176 | peerDependencies: 177 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 178 | 179 | acorn@8.10.0: 180 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 181 | engines: {node: '>=0.4.0'} 182 | hasBin: true 183 | 184 | ajv@6.12.6: 185 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 186 | 187 | ansi-regex@5.0.1: 188 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 189 | engines: {node: '>=8'} 190 | 191 | ansi-styles@4.3.0: 192 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 193 | engines: {node: '>=8'} 194 | 195 | argparse@2.0.1: 196 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 197 | 198 | array-union@2.1.0: 199 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 200 | engines: {node: '>=8'} 201 | 202 | balanced-match@1.0.2: 203 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 204 | 205 | brace-expansion@1.1.11: 206 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 207 | 208 | braces@3.0.2: 209 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 210 | engines: {node: '>=8'} 211 | 212 | builtin-modules@3.3.0: 213 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 214 | engines: {node: '>=6'} 215 | 216 | callsites@3.1.0: 217 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 218 | engines: {node: '>=6'} 219 | 220 | chalk@4.1.2: 221 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 222 | engines: {node: '>=10'} 223 | 224 | color-convert@2.0.1: 225 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 226 | engines: {node: '>=7.0.0'} 227 | 228 | color-name@1.1.4: 229 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 230 | 231 | concat-map@0.0.1: 232 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 233 | 234 | cross-spawn@7.0.3: 235 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 236 | engines: {node: '>= 8'} 237 | 238 | debug@4.3.4: 239 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 240 | engines: {node: '>=6.0'} 241 | peerDependencies: 242 | supports-color: '*' 243 | peerDependenciesMeta: 244 | supports-color: 245 | optional: true 246 | 247 | deep-is@0.1.4: 248 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 249 | 250 | dir-glob@3.0.1: 251 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 252 | engines: {node: '>=8'} 253 | 254 | doctrine@3.0.0: 255 | resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} 256 | engines: {node: '>=6.0.0'} 257 | 258 | esbuild-android-64@0.14.47: 259 | resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} 260 | engines: {node: '>=12'} 261 | cpu: [x64] 262 | os: [android] 263 | 264 | esbuild-android-arm64@0.14.47: 265 | resolution: {integrity: sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==} 266 | engines: {node: '>=12'} 267 | cpu: [arm64] 268 | os: [android] 269 | 270 | esbuild-darwin-64@0.14.47: 271 | resolution: {integrity: sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==} 272 | engines: {node: '>=12'} 273 | cpu: [x64] 274 | os: [darwin] 275 | 276 | esbuild-darwin-arm64@0.14.47: 277 | resolution: {integrity: sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==} 278 | engines: {node: '>=12'} 279 | cpu: [arm64] 280 | os: [darwin] 281 | 282 | esbuild-freebsd-64@0.14.47: 283 | resolution: {integrity: sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==} 284 | engines: {node: '>=12'} 285 | cpu: [x64] 286 | os: [freebsd] 287 | 288 | esbuild-freebsd-arm64@0.14.47: 289 | resolution: {integrity: sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==} 290 | engines: {node: '>=12'} 291 | cpu: [arm64] 292 | os: [freebsd] 293 | 294 | esbuild-linux-32@0.14.47: 295 | resolution: {integrity: sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==} 296 | engines: {node: '>=12'} 297 | cpu: [ia32] 298 | os: [linux] 299 | 300 | esbuild-linux-64@0.14.47: 301 | resolution: {integrity: sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==} 302 | engines: {node: '>=12'} 303 | cpu: [x64] 304 | os: [linux] 305 | 306 | esbuild-linux-arm64@0.14.47: 307 | resolution: {integrity: sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==} 308 | engines: {node: '>=12'} 309 | cpu: [arm64] 310 | os: [linux] 311 | 312 | esbuild-linux-arm@0.14.47: 313 | resolution: {integrity: sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==} 314 | engines: {node: '>=12'} 315 | cpu: [arm] 316 | os: [linux] 317 | 318 | esbuild-linux-mips64le@0.14.47: 319 | resolution: {integrity: sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==} 320 | engines: {node: '>=12'} 321 | cpu: [mips64el] 322 | os: [linux] 323 | 324 | esbuild-linux-ppc64le@0.14.47: 325 | resolution: {integrity: sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==} 326 | engines: {node: '>=12'} 327 | cpu: [ppc64] 328 | os: [linux] 329 | 330 | esbuild-linux-riscv64@0.14.47: 331 | resolution: {integrity: sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==} 332 | engines: {node: '>=12'} 333 | cpu: [riscv64] 334 | os: [linux] 335 | 336 | esbuild-linux-s390x@0.14.47: 337 | resolution: {integrity: sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==} 338 | engines: {node: '>=12'} 339 | cpu: [s390x] 340 | os: [linux] 341 | 342 | esbuild-netbsd-64@0.14.47: 343 | resolution: {integrity: sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==} 344 | engines: {node: '>=12'} 345 | cpu: [x64] 346 | os: [netbsd] 347 | 348 | esbuild-openbsd-64@0.14.47: 349 | resolution: {integrity: sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==} 350 | engines: {node: '>=12'} 351 | cpu: [x64] 352 | os: [openbsd] 353 | 354 | esbuild-sunos-64@0.14.47: 355 | resolution: {integrity: sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==} 356 | engines: {node: '>=12'} 357 | cpu: [x64] 358 | os: [sunos] 359 | 360 | esbuild-windows-32@0.14.47: 361 | resolution: {integrity: sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==} 362 | engines: {node: '>=12'} 363 | cpu: [ia32] 364 | os: [win32] 365 | 366 | esbuild-windows-64@0.14.47: 367 | resolution: {integrity: sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==} 368 | engines: {node: '>=12'} 369 | cpu: [x64] 370 | os: [win32] 371 | 372 | esbuild-windows-arm64@0.14.47: 373 | resolution: {integrity: sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==} 374 | engines: {node: '>=12'} 375 | cpu: [arm64] 376 | os: [win32] 377 | 378 | esbuild@0.14.47: 379 | resolution: {integrity: sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==} 380 | engines: {node: '>=12'} 381 | hasBin: true 382 | 383 | escape-string-regexp@4.0.0: 384 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 385 | engines: {node: '>=10'} 386 | 387 | eslint-scope@5.1.1: 388 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 389 | engines: {node: '>=8.0.0'} 390 | 391 | eslint-scope@7.2.2: 392 | resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} 393 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 394 | 395 | eslint-utils@3.0.0: 396 | resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} 397 | engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} 398 | peerDependencies: 399 | eslint: '>=5' 400 | 401 | eslint-visitor-keys@2.1.0: 402 | resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} 403 | engines: {node: '>=10'} 404 | 405 | eslint-visitor-keys@3.4.3: 406 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 407 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 408 | 409 | eslint@8.48.0: 410 | resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} 411 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 412 | deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. 413 | hasBin: true 414 | 415 | espree@9.6.1: 416 | resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} 417 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 418 | 419 | esquery@1.5.0: 420 | resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} 421 | engines: {node: '>=0.10'} 422 | 423 | esrecurse@4.3.0: 424 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 425 | engines: {node: '>=4.0'} 426 | 427 | estraverse@4.3.0: 428 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 429 | engines: {node: '>=4.0'} 430 | 431 | estraverse@5.3.0: 432 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 433 | engines: {node: '>=4.0'} 434 | 435 | esutils@2.0.3: 436 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 437 | engines: {node: '>=0.10.0'} 438 | 439 | fast-deep-equal@3.1.3: 440 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 441 | 442 | fast-glob@3.3.1: 443 | resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} 444 | engines: {node: '>=8.6.0'} 445 | 446 | fast-json-stable-stringify@2.1.0: 447 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 448 | 449 | fast-levenshtein@2.0.6: 450 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 451 | 452 | fastq@1.15.0: 453 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 454 | 455 | file-entry-cache@6.0.1: 456 | resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 457 | engines: {node: ^10.12.0 || >=12.0.0} 458 | 459 | fill-range@7.0.1: 460 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 461 | engines: {node: '>=8'} 462 | 463 | find-up@5.0.0: 464 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 465 | engines: {node: '>=10'} 466 | 467 | flat-cache@3.1.0: 468 | resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} 469 | engines: {node: '>=12.0.0'} 470 | 471 | flatted@3.2.7: 472 | resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} 473 | 474 | fs.realpath@1.0.0: 475 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 476 | 477 | functional-red-black-tree@1.0.1: 478 | resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} 479 | 480 | glob-parent@5.1.2: 481 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 482 | engines: {node: '>= 6'} 483 | 484 | glob-parent@6.0.2: 485 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 486 | engines: {node: '>=10.13.0'} 487 | 488 | glob@7.2.3: 489 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 490 | deprecated: Glob versions prior to v9 are no longer supported 491 | 492 | globals@13.21.0: 493 | resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} 494 | engines: {node: '>=8'} 495 | 496 | globby@11.1.0: 497 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 498 | engines: {node: '>=10'} 499 | 500 | graphemer@1.4.0: 501 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 502 | 503 | has-flag@4.0.0: 504 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 505 | engines: {node: '>=8'} 506 | 507 | ignore@5.2.4: 508 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 509 | engines: {node: '>= 4'} 510 | 511 | import-fresh@3.3.0: 512 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 513 | engines: {node: '>=6'} 514 | 515 | imurmurhash@0.1.4: 516 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 517 | engines: {node: '>=0.8.19'} 518 | 519 | inflight@1.0.6: 520 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 521 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 522 | 523 | inherits@2.0.4: 524 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 525 | 526 | is-extglob@2.1.1: 527 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 528 | engines: {node: '>=0.10.0'} 529 | 530 | is-glob@4.0.3: 531 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 532 | engines: {node: '>=0.10.0'} 533 | 534 | is-number@7.0.0: 535 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 536 | engines: {node: '>=0.12.0'} 537 | 538 | is-path-inside@3.0.3: 539 | resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 540 | engines: {node: '>=8'} 541 | 542 | isexe@2.0.0: 543 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 544 | 545 | js-yaml@4.1.0: 546 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 547 | hasBin: true 548 | 549 | json-buffer@3.0.1: 550 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 551 | 552 | json-schema-traverse@0.4.1: 553 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 554 | 555 | json-stable-stringify-without-jsonify@1.0.1: 556 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 557 | 558 | keyv@4.5.3: 559 | resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} 560 | 561 | levn@0.4.1: 562 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 563 | engines: {node: '>= 0.8.0'} 564 | 565 | locate-path@6.0.0: 566 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 567 | engines: {node: '>=10'} 568 | 569 | lodash.merge@4.6.2: 570 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 571 | 572 | lru-cache@6.0.0: 573 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 574 | engines: {node: '>=10'} 575 | 576 | merge2@1.4.1: 577 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 578 | engines: {node: '>= 8'} 579 | 580 | micromatch@4.0.5: 581 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 582 | engines: {node: '>=8.6'} 583 | 584 | minimatch@3.1.2: 585 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 586 | 587 | moment@2.29.4: 588 | resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} 589 | 590 | monkey-around@2.3.0: 591 | resolution: {integrity: sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA==} 592 | 593 | ms@2.1.2: 594 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 595 | 596 | natural-compare@1.4.0: 597 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 598 | 599 | obsidian@1.8.7: 600 | resolution: {integrity: sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==} 601 | peerDependencies: 602 | '@codemirror/state': ^6.0.0 603 | '@codemirror/view': ^6.0.0 604 | 605 | once@1.4.0: 606 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 607 | 608 | optionator@0.9.3: 609 | resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} 610 | engines: {node: '>= 0.8.0'} 611 | 612 | p-limit@3.1.0: 613 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 614 | engines: {node: '>=10'} 615 | 616 | p-locate@5.0.0: 617 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 618 | engines: {node: '>=10'} 619 | 620 | parent-module@1.0.1: 621 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 622 | engines: {node: '>=6'} 623 | 624 | path-exists@4.0.0: 625 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 626 | engines: {node: '>=8'} 627 | 628 | path-is-absolute@1.0.1: 629 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 630 | engines: {node: '>=0.10.0'} 631 | 632 | path-key@3.1.1: 633 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 634 | engines: {node: '>=8'} 635 | 636 | path-type@4.0.0: 637 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 638 | engines: {node: '>=8'} 639 | 640 | picomatch@2.3.1: 641 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 642 | engines: {node: '>=8.6'} 643 | 644 | prelude-ls@1.2.1: 645 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 646 | engines: {node: '>= 0.8.0'} 647 | 648 | punycode@2.3.0: 649 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 650 | engines: {node: '>=6'} 651 | 652 | queue-microtask@1.2.3: 653 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 654 | 655 | regexpp@3.2.0: 656 | resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} 657 | engines: {node: '>=8'} 658 | 659 | resolve-from@4.0.0: 660 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 661 | engines: {node: '>=4'} 662 | 663 | reusify@1.0.4: 664 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 665 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 666 | 667 | rimraf@3.0.2: 668 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 669 | deprecated: Rimraf versions prior to v4 are no longer supported 670 | hasBin: true 671 | 672 | run-parallel@1.2.0: 673 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 674 | 675 | semver@7.5.4: 676 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} 677 | engines: {node: '>=10'} 678 | hasBin: true 679 | 680 | shebang-command@2.0.0: 681 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 682 | engines: {node: '>=8'} 683 | 684 | shebang-regex@3.0.0: 685 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 686 | engines: {node: '>=8'} 687 | 688 | slash@3.0.0: 689 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 690 | engines: {node: '>=8'} 691 | 692 | strip-ansi@6.0.1: 693 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 694 | engines: {node: '>=8'} 695 | 696 | strip-json-comments@3.1.1: 697 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 698 | engines: {node: '>=8'} 699 | 700 | style-mod@4.1.0: 701 | resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==} 702 | 703 | supports-color@7.2.0: 704 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 705 | engines: {node: '>=8'} 706 | 707 | text-table@0.2.0: 708 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 709 | 710 | to-regex-range@5.0.1: 711 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 712 | engines: {node: '>=8.0'} 713 | 714 | tslib@1.14.1: 715 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 716 | 717 | tslib@2.4.0: 718 | resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} 719 | 720 | tsutils@3.21.0: 721 | resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} 722 | engines: {node: '>= 6'} 723 | peerDependencies: 724 | typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' 725 | 726 | type-check@0.4.0: 727 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 728 | engines: {node: '>= 0.8.0'} 729 | 730 | type-fest@0.20.2: 731 | resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} 732 | engines: {node: '>=10'} 733 | 734 | typescript@4.7.4: 735 | resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} 736 | engines: {node: '>=4.2.0'} 737 | hasBin: true 738 | 739 | uri-js@4.4.1: 740 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 741 | 742 | w3c-keyname@2.2.8: 743 | resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 744 | 745 | which@2.0.2: 746 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 747 | engines: {node: '>= 8'} 748 | hasBin: true 749 | 750 | wrappy@1.0.2: 751 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 752 | 753 | yallist@4.0.0: 754 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 755 | 756 | yocto-queue@0.1.0: 757 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 758 | engines: {node: '>=10'} 759 | 760 | snapshots: 761 | 762 | '@aashutoshrathi/word-wrap@1.2.6': {} 763 | 764 | '@codemirror/state@6.4.1': {} 765 | 766 | '@codemirror/view@6.28.1': 767 | dependencies: 768 | '@codemirror/state': 6.4.1 769 | style-mod: 4.1.0 770 | w3c-keyname: 2.2.8 771 | 772 | '@eslint-community/eslint-utils@4.4.0(eslint@8.48.0)': 773 | dependencies: 774 | eslint: 8.48.0 775 | eslint-visitor-keys: 3.4.3 776 | 777 | '@eslint-community/regexpp@4.8.0': {} 778 | 779 | '@eslint/eslintrc@2.1.2': 780 | dependencies: 781 | ajv: 6.12.6 782 | debug: 4.3.4 783 | espree: 9.6.1 784 | globals: 13.21.0 785 | ignore: 5.2.4 786 | import-fresh: 3.3.0 787 | js-yaml: 4.1.0 788 | minimatch: 3.1.2 789 | strip-json-comments: 3.1.1 790 | transitivePeerDependencies: 791 | - supports-color 792 | 793 | '@eslint/js@8.48.0': {} 794 | 795 | '@humanwhocodes/config-array@0.11.10': 796 | dependencies: 797 | '@humanwhocodes/object-schema': 1.2.1 798 | debug: 4.3.4 799 | minimatch: 3.1.2 800 | transitivePeerDependencies: 801 | - supports-color 802 | 803 | '@humanwhocodes/module-importer@1.0.1': {} 804 | 805 | '@humanwhocodes/object-schema@1.2.1': {} 806 | 807 | '@nodelib/fs.scandir@2.1.5': 808 | dependencies: 809 | '@nodelib/fs.stat': 2.0.5 810 | run-parallel: 1.2.0 811 | 812 | '@nodelib/fs.stat@2.0.5': {} 813 | 814 | '@nodelib/fs.walk@1.2.8': 815 | dependencies: 816 | '@nodelib/fs.scandir': 2.1.5 817 | fastq: 1.15.0 818 | 819 | '@types/codemirror@5.60.8': 820 | dependencies: 821 | '@types/tern': 0.23.4 822 | 823 | '@types/estree@1.0.1': {} 824 | 825 | '@types/json-schema@7.0.12': {} 826 | 827 | '@types/node@16.18.46': {} 828 | 829 | '@types/tern@0.23.4': 830 | dependencies: 831 | '@types/estree': 1.0.1 832 | 833 | '@typescript-eslint/eslint-plugin@5.29.0(@typescript-eslint/parser@5.29.0(eslint@8.48.0)(typescript@4.7.4))(eslint@8.48.0)(typescript@4.7.4)': 834 | dependencies: 835 | '@typescript-eslint/parser': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 836 | '@typescript-eslint/scope-manager': 5.29.0 837 | '@typescript-eslint/type-utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 838 | '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 839 | debug: 4.3.4 840 | eslint: 8.48.0 841 | functional-red-black-tree: 1.0.1 842 | ignore: 5.2.4 843 | regexpp: 3.2.0 844 | semver: 7.5.4 845 | tsutils: 3.21.0(typescript@4.7.4) 846 | optionalDependencies: 847 | typescript: 4.7.4 848 | transitivePeerDependencies: 849 | - supports-color 850 | 851 | '@typescript-eslint/parser@5.29.0(eslint@8.48.0)(typescript@4.7.4)': 852 | dependencies: 853 | '@typescript-eslint/scope-manager': 5.29.0 854 | '@typescript-eslint/types': 5.29.0 855 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 856 | debug: 4.3.4 857 | eslint: 8.48.0 858 | optionalDependencies: 859 | typescript: 4.7.4 860 | transitivePeerDependencies: 861 | - supports-color 862 | 863 | '@typescript-eslint/scope-manager@5.29.0': 864 | dependencies: 865 | '@typescript-eslint/types': 5.29.0 866 | '@typescript-eslint/visitor-keys': 5.29.0 867 | 868 | '@typescript-eslint/type-utils@5.29.0(eslint@8.48.0)(typescript@4.7.4)': 869 | dependencies: 870 | '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 871 | debug: 4.3.4 872 | eslint: 8.48.0 873 | tsutils: 3.21.0(typescript@4.7.4) 874 | optionalDependencies: 875 | typescript: 4.7.4 876 | transitivePeerDependencies: 877 | - supports-color 878 | 879 | '@typescript-eslint/types@5.29.0': {} 880 | 881 | '@typescript-eslint/typescript-estree@5.29.0(typescript@4.7.4)': 882 | dependencies: 883 | '@typescript-eslint/types': 5.29.0 884 | '@typescript-eslint/visitor-keys': 5.29.0 885 | debug: 4.3.4 886 | globby: 11.1.0 887 | is-glob: 4.0.3 888 | semver: 7.5.4 889 | tsutils: 3.21.0(typescript@4.7.4) 890 | optionalDependencies: 891 | typescript: 4.7.4 892 | transitivePeerDependencies: 893 | - supports-color 894 | 895 | '@typescript-eslint/utils@5.29.0(eslint@8.48.0)(typescript@4.7.4)': 896 | dependencies: 897 | '@types/json-schema': 7.0.12 898 | '@typescript-eslint/scope-manager': 5.29.0 899 | '@typescript-eslint/types': 5.29.0 900 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 901 | eslint: 8.48.0 902 | eslint-scope: 5.1.1 903 | eslint-utils: 3.0.0(eslint@8.48.0) 904 | transitivePeerDependencies: 905 | - supports-color 906 | - typescript 907 | 908 | '@typescript-eslint/visitor-keys@5.29.0': 909 | dependencies: 910 | '@typescript-eslint/types': 5.29.0 911 | eslint-visitor-keys: 3.4.3 912 | 913 | acorn-jsx@5.3.2(acorn@8.10.0): 914 | dependencies: 915 | acorn: 8.10.0 916 | 917 | acorn@8.10.0: {} 918 | 919 | ajv@6.12.6: 920 | dependencies: 921 | fast-deep-equal: 3.1.3 922 | fast-json-stable-stringify: 2.1.0 923 | json-schema-traverse: 0.4.1 924 | uri-js: 4.4.1 925 | 926 | ansi-regex@5.0.1: {} 927 | 928 | ansi-styles@4.3.0: 929 | dependencies: 930 | color-convert: 2.0.1 931 | 932 | argparse@2.0.1: {} 933 | 934 | array-union@2.1.0: {} 935 | 936 | balanced-match@1.0.2: {} 937 | 938 | brace-expansion@1.1.11: 939 | dependencies: 940 | balanced-match: 1.0.2 941 | concat-map: 0.0.1 942 | 943 | braces@3.0.2: 944 | dependencies: 945 | fill-range: 7.0.1 946 | 947 | builtin-modules@3.3.0: {} 948 | 949 | callsites@3.1.0: {} 950 | 951 | chalk@4.1.2: 952 | dependencies: 953 | ansi-styles: 4.3.0 954 | supports-color: 7.2.0 955 | 956 | color-convert@2.0.1: 957 | dependencies: 958 | color-name: 1.1.4 959 | 960 | color-name@1.1.4: {} 961 | 962 | concat-map@0.0.1: {} 963 | 964 | cross-spawn@7.0.3: 965 | dependencies: 966 | path-key: 3.1.1 967 | shebang-command: 2.0.0 968 | which: 2.0.2 969 | 970 | debug@4.3.4: 971 | dependencies: 972 | ms: 2.1.2 973 | 974 | deep-is@0.1.4: {} 975 | 976 | dir-glob@3.0.1: 977 | dependencies: 978 | path-type: 4.0.0 979 | 980 | doctrine@3.0.0: 981 | dependencies: 982 | esutils: 2.0.3 983 | 984 | esbuild-android-64@0.14.47: 985 | optional: true 986 | 987 | esbuild-android-arm64@0.14.47: 988 | optional: true 989 | 990 | esbuild-darwin-64@0.14.47: 991 | optional: true 992 | 993 | esbuild-darwin-arm64@0.14.47: 994 | optional: true 995 | 996 | esbuild-freebsd-64@0.14.47: 997 | optional: true 998 | 999 | esbuild-freebsd-arm64@0.14.47: 1000 | optional: true 1001 | 1002 | esbuild-linux-32@0.14.47: 1003 | optional: true 1004 | 1005 | esbuild-linux-64@0.14.47: 1006 | optional: true 1007 | 1008 | esbuild-linux-arm64@0.14.47: 1009 | optional: true 1010 | 1011 | esbuild-linux-arm@0.14.47: 1012 | optional: true 1013 | 1014 | esbuild-linux-mips64le@0.14.47: 1015 | optional: true 1016 | 1017 | esbuild-linux-ppc64le@0.14.47: 1018 | optional: true 1019 | 1020 | esbuild-linux-riscv64@0.14.47: 1021 | optional: true 1022 | 1023 | esbuild-linux-s390x@0.14.47: 1024 | optional: true 1025 | 1026 | esbuild-netbsd-64@0.14.47: 1027 | optional: true 1028 | 1029 | esbuild-openbsd-64@0.14.47: 1030 | optional: true 1031 | 1032 | esbuild-sunos-64@0.14.47: 1033 | optional: true 1034 | 1035 | esbuild-windows-32@0.14.47: 1036 | optional: true 1037 | 1038 | esbuild-windows-64@0.14.47: 1039 | optional: true 1040 | 1041 | esbuild-windows-arm64@0.14.47: 1042 | optional: true 1043 | 1044 | esbuild@0.14.47: 1045 | optionalDependencies: 1046 | esbuild-android-64: 0.14.47 1047 | esbuild-android-arm64: 0.14.47 1048 | esbuild-darwin-64: 0.14.47 1049 | esbuild-darwin-arm64: 0.14.47 1050 | esbuild-freebsd-64: 0.14.47 1051 | esbuild-freebsd-arm64: 0.14.47 1052 | esbuild-linux-32: 0.14.47 1053 | esbuild-linux-64: 0.14.47 1054 | esbuild-linux-arm: 0.14.47 1055 | esbuild-linux-arm64: 0.14.47 1056 | esbuild-linux-mips64le: 0.14.47 1057 | esbuild-linux-ppc64le: 0.14.47 1058 | esbuild-linux-riscv64: 0.14.47 1059 | esbuild-linux-s390x: 0.14.47 1060 | esbuild-netbsd-64: 0.14.47 1061 | esbuild-openbsd-64: 0.14.47 1062 | esbuild-sunos-64: 0.14.47 1063 | esbuild-windows-32: 0.14.47 1064 | esbuild-windows-64: 0.14.47 1065 | esbuild-windows-arm64: 0.14.47 1066 | 1067 | escape-string-regexp@4.0.0: {} 1068 | 1069 | eslint-scope@5.1.1: 1070 | dependencies: 1071 | esrecurse: 4.3.0 1072 | estraverse: 4.3.0 1073 | 1074 | eslint-scope@7.2.2: 1075 | dependencies: 1076 | esrecurse: 4.3.0 1077 | estraverse: 5.3.0 1078 | 1079 | eslint-utils@3.0.0(eslint@8.48.0): 1080 | dependencies: 1081 | eslint: 8.48.0 1082 | eslint-visitor-keys: 2.1.0 1083 | 1084 | eslint-visitor-keys@2.1.0: {} 1085 | 1086 | eslint-visitor-keys@3.4.3: {} 1087 | 1088 | eslint@8.48.0: 1089 | dependencies: 1090 | '@eslint-community/eslint-utils': 4.4.0(eslint@8.48.0) 1091 | '@eslint-community/regexpp': 4.8.0 1092 | '@eslint/eslintrc': 2.1.2 1093 | '@eslint/js': 8.48.0 1094 | '@humanwhocodes/config-array': 0.11.10 1095 | '@humanwhocodes/module-importer': 1.0.1 1096 | '@nodelib/fs.walk': 1.2.8 1097 | ajv: 6.12.6 1098 | chalk: 4.1.2 1099 | cross-spawn: 7.0.3 1100 | debug: 4.3.4 1101 | doctrine: 3.0.0 1102 | escape-string-regexp: 4.0.0 1103 | eslint-scope: 7.2.2 1104 | eslint-visitor-keys: 3.4.3 1105 | espree: 9.6.1 1106 | esquery: 1.5.0 1107 | esutils: 2.0.3 1108 | fast-deep-equal: 3.1.3 1109 | file-entry-cache: 6.0.1 1110 | find-up: 5.0.0 1111 | glob-parent: 6.0.2 1112 | globals: 13.21.0 1113 | graphemer: 1.4.0 1114 | ignore: 5.2.4 1115 | imurmurhash: 0.1.4 1116 | is-glob: 4.0.3 1117 | is-path-inside: 3.0.3 1118 | js-yaml: 4.1.0 1119 | json-stable-stringify-without-jsonify: 1.0.1 1120 | levn: 0.4.1 1121 | lodash.merge: 4.6.2 1122 | minimatch: 3.1.2 1123 | natural-compare: 1.4.0 1124 | optionator: 0.9.3 1125 | strip-ansi: 6.0.1 1126 | text-table: 0.2.0 1127 | transitivePeerDependencies: 1128 | - supports-color 1129 | 1130 | espree@9.6.1: 1131 | dependencies: 1132 | acorn: 8.10.0 1133 | acorn-jsx: 5.3.2(acorn@8.10.0) 1134 | eslint-visitor-keys: 3.4.3 1135 | 1136 | esquery@1.5.0: 1137 | dependencies: 1138 | estraverse: 5.3.0 1139 | 1140 | esrecurse@4.3.0: 1141 | dependencies: 1142 | estraverse: 5.3.0 1143 | 1144 | estraverse@4.3.0: {} 1145 | 1146 | estraverse@5.3.0: {} 1147 | 1148 | esutils@2.0.3: {} 1149 | 1150 | fast-deep-equal@3.1.3: {} 1151 | 1152 | fast-glob@3.3.1: 1153 | dependencies: 1154 | '@nodelib/fs.stat': 2.0.5 1155 | '@nodelib/fs.walk': 1.2.8 1156 | glob-parent: 5.1.2 1157 | merge2: 1.4.1 1158 | micromatch: 4.0.5 1159 | 1160 | fast-json-stable-stringify@2.1.0: {} 1161 | 1162 | fast-levenshtein@2.0.6: {} 1163 | 1164 | fastq@1.15.0: 1165 | dependencies: 1166 | reusify: 1.0.4 1167 | 1168 | file-entry-cache@6.0.1: 1169 | dependencies: 1170 | flat-cache: 3.1.0 1171 | 1172 | fill-range@7.0.1: 1173 | dependencies: 1174 | to-regex-range: 5.0.1 1175 | 1176 | find-up@5.0.0: 1177 | dependencies: 1178 | locate-path: 6.0.0 1179 | path-exists: 4.0.0 1180 | 1181 | flat-cache@3.1.0: 1182 | dependencies: 1183 | flatted: 3.2.7 1184 | keyv: 4.5.3 1185 | rimraf: 3.0.2 1186 | 1187 | flatted@3.2.7: {} 1188 | 1189 | fs.realpath@1.0.0: {} 1190 | 1191 | functional-red-black-tree@1.0.1: {} 1192 | 1193 | glob-parent@5.1.2: 1194 | dependencies: 1195 | is-glob: 4.0.3 1196 | 1197 | glob-parent@6.0.2: 1198 | dependencies: 1199 | is-glob: 4.0.3 1200 | 1201 | glob@7.2.3: 1202 | dependencies: 1203 | fs.realpath: 1.0.0 1204 | inflight: 1.0.6 1205 | inherits: 2.0.4 1206 | minimatch: 3.1.2 1207 | once: 1.4.0 1208 | path-is-absolute: 1.0.1 1209 | 1210 | globals@13.21.0: 1211 | dependencies: 1212 | type-fest: 0.20.2 1213 | 1214 | globby@11.1.0: 1215 | dependencies: 1216 | array-union: 2.1.0 1217 | dir-glob: 3.0.1 1218 | fast-glob: 3.3.1 1219 | ignore: 5.2.4 1220 | merge2: 1.4.1 1221 | slash: 3.0.0 1222 | 1223 | graphemer@1.4.0: {} 1224 | 1225 | has-flag@4.0.0: {} 1226 | 1227 | ignore@5.2.4: {} 1228 | 1229 | import-fresh@3.3.0: 1230 | dependencies: 1231 | parent-module: 1.0.1 1232 | resolve-from: 4.0.0 1233 | 1234 | imurmurhash@0.1.4: {} 1235 | 1236 | inflight@1.0.6: 1237 | dependencies: 1238 | once: 1.4.0 1239 | wrappy: 1.0.2 1240 | 1241 | inherits@2.0.4: {} 1242 | 1243 | is-extglob@2.1.1: {} 1244 | 1245 | is-glob@4.0.3: 1246 | dependencies: 1247 | is-extglob: 2.1.1 1248 | 1249 | is-number@7.0.0: {} 1250 | 1251 | is-path-inside@3.0.3: {} 1252 | 1253 | isexe@2.0.0: {} 1254 | 1255 | js-yaml@4.1.0: 1256 | dependencies: 1257 | argparse: 2.0.1 1258 | 1259 | json-buffer@3.0.1: {} 1260 | 1261 | json-schema-traverse@0.4.1: {} 1262 | 1263 | json-stable-stringify-without-jsonify@1.0.1: {} 1264 | 1265 | keyv@4.5.3: 1266 | dependencies: 1267 | json-buffer: 3.0.1 1268 | 1269 | levn@0.4.1: 1270 | dependencies: 1271 | prelude-ls: 1.2.1 1272 | type-check: 0.4.0 1273 | 1274 | locate-path@6.0.0: 1275 | dependencies: 1276 | p-locate: 5.0.0 1277 | 1278 | lodash.merge@4.6.2: {} 1279 | 1280 | lru-cache@6.0.0: 1281 | dependencies: 1282 | yallist: 4.0.0 1283 | 1284 | merge2@1.4.1: {} 1285 | 1286 | micromatch@4.0.5: 1287 | dependencies: 1288 | braces: 3.0.2 1289 | picomatch: 2.3.1 1290 | 1291 | minimatch@3.1.2: 1292 | dependencies: 1293 | brace-expansion: 1.1.11 1294 | 1295 | moment@2.29.4: {} 1296 | 1297 | monkey-around@2.3.0: {} 1298 | 1299 | ms@2.1.2: {} 1300 | 1301 | natural-compare@1.4.0: {} 1302 | 1303 | obsidian@1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.28.1): 1304 | dependencies: 1305 | '@codemirror/state': 6.4.1 1306 | '@codemirror/view': 6.28.1 1307 | '@types/codemirror': 5.60.8 1308 | moment: 2.29.4 1309 | 1310 | once@1.4.0: 1311 | dependencies: 1312 | wrappy: 1.0.2 1313 | 1314 | optionator@0.9.3: 1315 | dependencies: 1316 | '@aashutoshrathi/word-wrap': 1.2.6 1317 | deep-is: 0.1.4 1318 | fast-levenshtein: 2.0.6 1319 | levn: 0.4.1 1320 | prelude-ls: 1.2.1 1321 | type-check: 0.4.0 1322 | 1323 | p-limit@3.1.0: 1324 | dependencies: 1325 | yocto-queue: 0.1.0 1326 | 1327 | p-locate@5.0.0: 1328 | dependencies: 1329 | p-limit: 3.1.0 1330 | 1331 | parent-module@1.0.1: 1332 | dependencies: 1333 | callsites: 3.1.0 1334 | 1335 | path-exists@4.0.0: {} 1336 | 1337 | path-is-absolute@1.0.1: {} 1338 | 1339 | path-key@3.1.1: {} 1340 | 1341 | path-type@4.0.0: {} 1342 | 1343 | picomatch@2.3.1: {} 1344 | 1345 | prelude-ls@1.2.1: {} 1346 | 1347 | punycode@2.3.0: {} 1348 | 1349 | queue-microtask@1.2.3: {} 1350 | 1351 | regexpp@3.2.0: {} 1352 | 1353 | resolve-from@4.0.0: {} 1354 | 1355 | reusify@1.0.4: {} 1356 | 1357 | rimraf@3.0.2: 1358 | dependencies: 1359 | glob: 7.2.3 1360 | 1361 | run-parallel@1.2.0: 1362 | dependencies: 1363 | queue-microtask: 1.2.3 1364 | 1365 | semver@7.5.4: 1366 | dependencies: 1367 | lru-cache: 6.0.0 1368 | 1369 | shebang-command@2.0.0: 1370 | dependencies: 1371 | shebang-regex: 3.0.0 1372 | 1373 | shebang-regex@3.0.0: {} 1374 | 1375 | slash@3.0.0: {} 1376 | 1377 | strip-ansi@6.0.1: 1378 | dependencies: 1379 | ansi-regex: 5.0.1 1380 | 1381 | strip-json-comments@3.1.1: {} 1382 | 1383 | style-mod@4.1.0: {} 1384 | 1385 | supports-color@7.2.0: 1386 | dependencies: 1387 | has-flag: 4.0.0 1388 | 1389 | text-table@0.2.0: {} 1390 | 1391 | to-regex-range@5.0.1: 1392 | dependencies: 1393 | is-number: 7.0.0 1394 | 1395 | tslib@1.14.1: {} 1396 | 1397 | tslib@2.4.0: {} 1398 | 1399 | tsutils@3.21.0(typescript@4.7.4): 1400 | dependencies: 1401 | tslib: 1.14.1 1402 | typescript: 4.7.4 1403 | 1404 | type-check@0.4.0: 1405 | dependencies: 1406 | prelude-ls: 1.2.1 1407 | 1408 | type-fest@0.20.2: {} 1409 | 1410 | typescript@4.7.4: {} 1411 | 1412 | uri-js@4.4.1: 1413 | dependencies: 1414 | punycode: 2.3.0 1415 | 1416 | w3c-keyname@2.2.8: {} 1417 | 1418 | which@2.0.2: 1419 | dependencies: 1420 | isexe: 2.0.0 1421 | 1422 | wrappy@1.0.2: {} 1423 | 1424 | yallist@4.0.0: {} 1425 | 1426 | yocto-queue@0.1.0: {} 1427 | -------------------------------------------------------------------------------- /src/ControlHeader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type CanvasFileNode, 3 | type CanvasGroupNode, 4 | type CanvasLinkNode, 5 | type CanvasNode, 6 | type CanvasTextNode, 7 | Component, 8 | parseFrontMatterAliases, 9 | setIcon, 10 | } from "obsidian"; 11 | import type { HeaderComponent } from "./types/custom"; 12 | import type CanvasCollapsePlugin from "."; 13 | import type { AllCanvasNodeData } from "obsidian/canvas"; 14 | 15 | export default class CollapseControlHeader 16 | extends Component 17 | implements HeaderComponent 18 | { 19 | private collapsed = false; 20 | private collapsedIconEl: HTMLDivElement; 21 | private typeIconEl: HTMLDivElement; 22 | private titleEl: HTMLSpanElement; 23 | private headerEl: HTMLElement; 24 | private thumbnailEl: HTMLImageElement; 25 | private aliasEl: HTMLSpanElement; 26 | 27 | private content = ""; 28 | private node: CanvasNode; 29 | private alias = ""; 30 | private thumbnailUrl = ""; 31 | 32 | private refreshed = false; 33 | private containingNodes: CanvasNode[] = []; 34 | 35 | plugin: CanvasCollapsePlugin; 36 | oldFilePath = ""; 37 | 38 | constructor(plugin: CanvasCollapsePlugin, node: CanvasNode) { 39 | super(); 40 | 41 | this.plugin = plugin; 42 | this.node = node; 43 | this.collapsed = 44 | node.unknownData.collapsed === undefined 45 | ? false 46 | : node.unknownData.collapsed; 47 | } 48 | 49 | onload() { 50 | this.initHeader(); 51 | this.initContent(); 52 | this.initTypeIcon(); 53 | this.updateNodesInGroup(); 54 | this.updateNode(); 55 | 56 | this.registerEvent( 57 | this.plugin.app.vault.on("rename", (file, oldPath) => { 58 | if (oldPath === this.oldFilePath) { 59 | this.titleEl.setText(file.name.split(".")[0]); 60 | this.oldFilePath = file.path; 61 | } 62 | }) 63 | ); 64 | 65 | return this.headerEl; 66 | } 67 | 68 | onunload() { 69 | super.onunload(); 70 | } 71 | 72 | unload() { 73 | super.unload(); 74 | 75 | this.headerEl.empty(); 76 | this.headerEl.detach(); 77 | } 78 | 79 | initHeader() { 80 | this.headerEl = createEl("div", { 81 | cls: "canvas-node-collapse-control", 82 | }); 83 | this.registerDomEvent(this.headerEl, "click", async (evt) => { 84 | evt.preventDefault(); 85 | evt.stopPropagation(); 86 | await this.toggleCollapsed(); 87 | }); 88 | 89 | this.collapsedIconEl = this.headerEl.createEl("div", { 90 | cls: "canvas-node-collapse-control-icon", 91 | }); 92 | 93 | this.typeIconEl = this.headerEl.createEl("div", { 94 | cls: "canvas-node-type-icon", 95 | }); 96 | 97 | this.titleEl = this.headerEl.createEl("span", { 98 | cls: "canvas-node-collapse-control-title", 99 | }); 100 | 101 | this.thumbnailEl = this.node.nodeEl.createEl("img", { 102 | cls: "canvas-node-collapse-control-thumbnail", 103 | }); 104 | 105 | this.aliasEl = this.headerEl.createEl("span", { 106 | cls: "canvas-node-collapse-control-alias", 107 | }); 108 | } 109 | 110 | checkNodeType() { 111 | return this.node.unknownData.type; 112 | } 113 | 114 | initTypeIcon() { 115 | this.setIconOrContent("setIcon"); 116 | } 117 | 118 | initContent() { 119 | this.setIconOrContent("setContent"); 120 | this.titleEl.setText(this.content?.replace(/^\#{1,} /g, "")); 121 | this.initAlias(); 122 | this.initThumbnail(); 123 | } 124 | 125 | initAlias() { 126 | // Try to get alias from node metadata 127 | if (this.node.unknownData?.alias) { 128 | this.alias = this.node.unknownData.alias; 129 | } else { 130 | // For file nodes, try to get alias from frontmatter 131 | const fileNode = this.node as CanvasFileNode; 132 | if (fileNode.file && this.plugin.app.metadataCache) { 133 | try { 134 | const meta = this.plugin.app.metadataCache.getFileCache( 135 | fileNode.file 136 | ); 137 | if (meta?.frontmatter) { 138 | const aliases = parseFrontMatterAliases( 139 | meta.frontmatter 140 | ); 141 | if (aliases && aliases.length > 0) { 142 | this.alias = aliases[0]; 143 | } 144 | } 145 | } catch (e) { 146 | console.debug("Error getting alias:", e); 147 | } 148 | } 149 | } 150 | 151 | if (this.alias) { 152 | this.aliasEl.setText(this.alias); 153 | } else { 154 | this.aliasEl.detach(); 155 | } 156 | } 157 | 158 | updateNodeAlias(newAlias: string) { 159 | this.alias = newAlias; 160 | 161 | if (this.alias) { 162 | // Re-attach if it was detached 163 | if (!this.aliasEl.parentElement) { 164 | this.headerEl.appendChild(this.aliasEl); 165 | } 166 | this.aliasEl.setText(this.alias); 167 | } else { 168 | this.aliasEl.detach(); 169 | } 170 | } 171 | 172 | initThumbnail() { 173 | // Try to get thumbnail from node metadata 174 | if (this.node.unknownData?.thumbnail) { 175 | this.thumbnailUrl = this.node.unknownData.thumbnail; 176 | } else { 177 | // For file nodes, try to get thumbnail from frontmatter 178 | const fileNode = this.node as CanvasFileNode; 179 | if (fileNode.file && this.plugin.app.metadataCache) { 180 | try { 181 | const meta = this.plugin.app.metadataCache.getFileCache( 182 | fileNode.file 183 | ); 184 | if (meta?.frontmatter?.thumbnail) { 185 | this.thumbnailUrl = meta.frontmatter.thumbnail; 186 | } 187 | } catch (e) { 188 | console.debug("Error getting thumbnail:", e); 189 | } 190 | } 191 | } 192 | 193 | if (this.thumbnailUrl) { 194 | try { 195 | // Handle both absolute and relative paths 196 | const url = this.thumbnailUrl.startsWith("http") 197 | ? this.thumbnailUrl 198 | : this.plugin.app.vault.adapter.getResourcePath( 199 | this.thumbnailUrl 200 | ); 201 | 202 | this.thumbnailEl.src = url; 203 | } catch (e) { 204 | console.debug("Error setting thumbnail src:", e); 205 | this.thumbnailEl.detach(); 206 | } 207 | } else { 208 | this.thumbnailEl.detach(); 209 | } 210 | } 211 | 212 | updateNodeThumbnail(newThumbnailUrl: string) { 213 | this.thumbnailUrl = newThumbnailUrl; 214 | 215 | if (this.thumbnailUrl) { 216 | try { 217 | // Re-attach if it was detached 218 | if (!this.thumbnailEl.parentElement) { 219 | this.headerEl.appendChild(this.thumbnailEl); 220 | } 221 | 222 | // Handle both absolute and relative paths 223 | const url = this.thumbnailUrl.startsWith("http") 224 | ? this.thumbnailUrl 225 | : this.plugin.app.vault.adapter.getResourcePath( 226 | this.thumbnailUrl 227 | ); 228 | 229 | this.thumbnailEl.src = url; 230 | } catch (e) { 231 | console.debug("Error setting thumbnail src:", e); 232 | this.thumbnailEl.detach(); 233 | } 234 | } else { 235 | this.thumbnailEl.detach(); 236 | } 237 | } 238 | 239 | setIconOrContent(action: "setIcon" | "setContent") { 240 | const currentType = this.checkNodeType(); 241 | switch (currentType) { 242 | case "text": 243 | if (action === "setIcon") 244 | setIcon(this.typeIconEl, "sticky-note"); 245 | if (action === "setContent") 246 | this.content = 247 | (this.node as CanvasTextNode).text.slice(0, 10) + 248 | ((this.node as CanvasTextNode).text.length > 10 249 | ? "..." 250 | : ""); 251 | break; 252 | case "file": 253 | if (action === "setIcon") { 254 | if ( 255 | (this.node as CanvasFileNode).file.name 256 | .split(".")[1] 257 | .trim() === "md" 258 | ) 259 | setIcon(this.typeIconEl, "file-text"); 260 | else setIcon(this.typeIconEl, "file-image"); 261 | } 262 | if (action === "setContent") 263 | this.content = ( 264 | this.node as CanvasFileNode 265 | ).file?.name.split(".")[0]; 266 | this.oldFilePath = (this.node as CanvasFileNode).file?.path; 267 | break; 268 | case "group": 269 | if (action === "setIcon") 270 | setIcon(this.typeIconEl, "create-group"); 271 | if (action === "setContent") this.content = ""; 272 | break; 273 | case "link": 274 | if (action === "setIcon") setIcon(this.typeIconEl, "link"); 275 | if (action === "setContent") 276 | this.content = (this.node as CanvasLinkNode).url; 277 | break; 278 | } 279 | 280 | if (action === "setIcon" && !this.node.unknownData.type) { 281 | setIcon(this.typeIconEl, "sticky-note"); 282 | } 283 | } 284 | 285 | setCollapsed(collapsed: boolean) { 286 | if (this.node.canvas.readonly) return; 287 | if (this.collapsed === collapsed) return; 288 | 289 | this.collapsed = collapsed; 290 | this.node.unknownData.collapsed = collapsed; 291 | 292 | this.updateNodesInGroup(); 293 | this.updateNode(); 294 | this.updateEdges(); 295 | } 296 | 297 | refreshHistory() { 298 | if (this.refreshed) return; 299 | 300 | const history = this.node.canvas.history; 301 | if (!history || history.data.length === 0) return; 302 | 303 | for (const data of history.data) { 304 | for (const node of data.nodes) { 305 | if (node.id === this.node.id && node?.collapsed === undefined) { 306 | node.collapsed = false; 307 | } 308 | } 309 | } 310 | this.refreshed = true; 311 | } 312 | 313 | async toggleCollapsed() { 314 | if (this.node.canvas.readonly) return; 315 | const wasCollapsed = this.collapsed; 316 | this.collapsed = !this.collapsed; 317 | 318 | this.node.unknownData.collapsed = !this.collapsed; 319 | 320 | // Update visual state first for animation 321 | this.updateNode(); 322 | 323 | // Add delay to allow for animation to complete before updating other elements 324 | setTimeout(() => { 325 | // Update nodes in group after a short delay to allow animation to start 326 | this.updateNodesInGroup(); 327 | this.updateEdges(); 328 | }, 50); 329 | 330 | this.node.canvas.requestSave(false, true); 331 | const canvasCurrentData = this.node.canvas.getData(); 332 | const nodeData = canvasCurrentData.nodes.find( 333 | (node: AllCanvasNodeData) => node.id === this.node.id 334 | ); 335 | 336 | if (nodeData) { 337 | nodeData.collapsed = this.collapsed; 338 | 339 | // If this node was collapsed and is now being expanded, move it to the highest z-index 340 | if (wasCollapsed && !this.collapsed) { 341 | // Remove the node from its current position 342 | const nodeIndex = canvasCurrentData.nodes.findIndex( 343 | (node: AllCanvasNodeData) => node.id === this.node.id 344 | ); 345 | if (nodeIndex !== -1) { 346 | const removedNode = canvasCurrentData.nodes.splice( 347 | nodeIndex, 348 | 1 349 | )[0]; 350 | // Add it back at the end of the array (highest z-index) 351 | canvasCurrentData.nodes.push(removedNode); 352 | } 353 | } 354 | 355 | this.refreshHistory(); 356 | } 357 | 358 | setTimeout(() => { 359 | this.node.canvas.setData(canvasCurrentData); 360 | this.node.canvas.requestSave(true); 361 | }, 300); // Increased timeout to match animation duration 362 | } 363 | 364 | updateNode() { 365 | this.node.nodeEl.toggleClass("collapsed", this.collapsed); 366 | 367 | setIcon(this.collapsedIconEl, "chevron-down"); 368 | this.collapsedIconEl.toggleClass( 369 | ["collapsed", "collapse-handler"], 370 | this.collapsed 371 | ); 372 | // Handle thumbnails visibility 373 | this.updateThumbnailVisibility(); 374 | 375 | // Handle aliases visibility 376 | this.updateAliasVisibility(); 377 | } 378 | 379 | updateThumbnailVisibility() { 380 | if (this.collapsed || this.plugin.settings.showThumbnailsAlways) { 381 | if ( 382 | this.plugin.settings.showThumbnailsInCollapsedState && 383 | this.thumbnailUrl && 384 | this.thumbnailUrl !== this.node.unknownData.title 385 | ) { 386 | this.thumbnailEl.toggleClass("collapsed-node-hidden", false); 387 | // Hide the title if we're showing an alias instead 388 | this.titleEl.toggleClass("collapsed-node-hidden", true); 389 | } else { 390 | this.thumbnailEl.toggleClass("collapsed-node-hidden", true); 391 | // Make sure title is visible if alias isn't shown 392 | this.titleEl.toggleClass("collapsed-node-hidden", false); 393 | } 394 | } else { 395 | this.thumbnailEl.toggleClass("collapsed-node-hidden", true); 396 | } 397 | } 398 | 399 | updateAliasVisibility() { 400 | if (this.collapsed || this.plugin.settings.showAliasesAlways) { 401 | if ( 402 | this.plugin.settings.showAliasesInCollapsedState && 403 | this.alias && 404 | this.alias !== this.node.unknownData.title 405 | ) { 406 | this.aliasEl.toggleClass("collapsed-node-hidden", false); 407 | // Hide the title if we're showing an alias instead 408 | this.titleEl.toggleClass("collapsed-node-hidden", true); 409 | } else { 410 | this.aliasEl.toggleClass("collapsed-node-hidden", true); 411 | // Make sure title is visible if alias isn't shown 412 | this.titleEl.toggleClass("collapsed-node-hidden", false); 413 | } 414 | } else { 415 | this.aliasEl.toggleClass("collapsed-node-hidden", true); 416 | } 417 | } 418 | 419 | updateEdges() { 420 | this.node.canvas.nodeInteractionLayer.interactionEl.detach(); 421 | this.node.canvas.nodeInteractionLayer.render(); 422 | const edges = this.node.canvas.getEdgesForNode(this.node); 423 | for (const edge of edges) { 424 | edge.render(); 425 | } 426 | } 427 | 428 | updateNodesInGroup(expandAll?: boolean) { 429 | if ( 430 | this.node.unknownData.type === "group" || 431 | (this.node as CanvasGroupNode).label 432 | ) { 433 | const nodes = this.node.canvas.getContainingNodes( 434 | this.node.getBBox(true) 435 | ); 436 | 437 | if (expandAll) { 438 | this.collapsed = false; 439 | } 440 | 441 | // Add animation class to the group node 442 | this.node.nodeEl.toggleClass("animating", true); 443 | 444 | // Remove animation class after animation completes 445 | setTimeout(() => { 446 | this.node.nodeEl.toggleClass("animating", false); 447 | }, 300); 448 | 449 | if (this.collapsed) { 450 | const filteredNodes = nodes.filter( 451 | (node: CanvasNode) => node.id !== this.node.id 452 | ); 453 | for (const node of filteredNodes) { 454 | this.containingNodes.push(node); 455 | 456 | // Add transition class before changing state 457 | node.nodeEl.toggleClass("node-transitioning", true); 458 | 459 | // Apply the collapsed state 460 | node.nodeEl.toggleClass( 461 | "group-nodes-collapsed", 462 | this.collapsed 463 | ); 464 | 465 | // Remove transition class after animation completes 466 | setTimeout(() => { 467 | node.nodeEl.toggleClass("node-transitioning", false); 468 | }, 300); 469 | 470 | this.updateEdgesInGroup(node); 471 | } 472 | } else { 473 | const otherGroupNodes = nodes.filter( 474 | (node: CanvasNode) => 475 | node.id !== this.node.id && 476 | node.unknownData.type === "group" && 477 | node.unknownData.collapsed 478 | ); 479 | // Ignore those nodes in collapsed child group 480 | const ignoreNodes: CanvasNode[] = []; 481 | for (const groupNode of otherGroupNodes) { 482 | const bbox = groupNode.getBBox(true); 483 | const nodesInGroup = 484 | this.node.canvas.getContainingNodes(bbox); 485 | for (const childNode of nodesInGroup) { 486 | if (childNode.id !== groupNode.id) { 487 | ignoreNodes.push(childNode); 488 | } 489 | } 490 | } 491 | 492 | const filteredContainingNodes = this.containingNodes.filter( 493 | (t) => !ignoreNodes.find((k) => k.id === t.id) 494 | ); 495 | 496 | for (const node of filteredContainingNodes) { 497 | // Add transition class before changing state 498 | node.nodeEl.toggleClass("node-transitioning", true); 499 | 500 | // Apply the expanded state 501 | node.nodeEl.toggleClass( 502 | "group-nodes-collapsed", 503 | this.collapsed 504 | ); 505 | 506 | // Remove transition class after animation completes 507 | setTimeout(() => { 508 | node.nodeEl.toggleClass("node-transitioning", false); 509 | }, 300); 510 | 511 | this.updateEdgesInGroup(node, true); 512 | } 513 | 514 | for (const node of ignoreNodes) { 515 | this.updateEdgesInGroup(node, node.unknownData.collapsed); 516 | } 517 | 518 | this.containingNodes = []; 519 | } 520 | this.updateEdges(); 521 | } 522 | } 523 | 524 | updateEdgesInGroup(node: CanvasNode, triggerCollapsed?: boolean) { 525 | const edges = this.node.canvas.getEdgesForNode(node); 526 | 527 | for (const edge of edges) { 528 | edge.labelElement?.wrapperEl?.classList.toggle( 529 | "group-edges-collapsed", 530 | triggerCollapsed || this.collapsed 531 | ); 532 | edge.lineGroupEl.classList.toggle( 533 | "group-edges-collapsed", 534 | triggerCollapsed || this.collapsed 535 | ); 536 | edge.lineEndGroupEl?.classList.toggle( 537 | "group-edges-collapsed", 538 | triggerCollapsed || this.collapsed 539 | ); 540 | edge.lineStartGroupEl?.classList.toggle( 541 | "group-edges-collapsed", 542 | triggerCollapsed || this.collapsed 543 | ); 544 | } 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addIcon, 3 | debounce, 4 | editorInfoField, 5 | Plugin, 6 | PluginSettingTab, 7 | Setting, 8 | } from "obsidian"; 9 | import CollapseControlHeader from "./ControlHeader"; 10 | import { 11 | handleNodeContextMenu, 12 | handleNodesViaCommands, 13 | handleSelectionContextMenu, 14 | refreshAllCanvasView, 15 | } from "./utils"; 16 | import { EditorView, ViewUpdate } from "@codemirror/view"; 17 | import { 18 | aroundCanvasMethods, 19 | patchCanvasInteraction, 20 | patchCanvasMenu, 21 | patchCanvasNode, 22 | } from "./patchUtils"; 23 | 24 | interface CollapsableNodeSettings { 25 | collapsableFileNode: boolean; 26 | collapsableAttachmentNode: boolean; 27 | collapsableGroupNode: boolean; 28 | collapsableLinkNode: boolean; 29 | collapsableTextNode: boolean; 30 | } 31 | 32 | interface OtherSettings { 33 | minLineAmount: number; 34 | minimalControlHeader: boolean; 35 | showThumbnailsInCollapsedState: boolean; 36 | showAliasesInCollapsedState: boolean; 37 | showAliasesAlways: boolean; 38 | showThumbnailsAlways: boolean; 39 | hideDefaultNodeTitle: boolean; 40 | } 41 | 42 | type CanvasCollapseSettings = CollapsableNodeSettings & OtherSettings; 43 | 44 | const DEFAULT_SETTINGS: CanvasCollapseSettings = { 45 | collapsableFileNode: true, 46 | collapsableAttachmentNode: true, 47 | collapsableGroupNode: true, 48 | collapsableLinkNode: true, 49 | collapsableTextNode: true, 50 | 51 | minLineAmount: 0, 52 | minimalControlHeader: false, 53 | showThumbnailsInCollapsedState: false, 54 | showAliasesInCollapsedState: false, 55 | showAliasesAlways: false, 56 | showThumbnailsAlways: false, 57 | hideDefaultNodeTitle: false, 58 | }; 59 | 60 | const DynamicUpdateControlHeader = (plugin: CanvasCollapsePlugin) => { 61 | return EditorView.updateListener.of((v: ViewUpdate) => { 62 | if (v.docChanged) { 63 | const editor = v.state.field(editorInfoField); 64 | const node = (editor as any).node; 65 | if (node) { 66 | if ( 67 | node.unknownData.type === "text" && 68 | !plugin.settings.collapsableTextNode 69 | ) 70 | return; 71 | if ( 72 | node.unknownData.type === "file" && 73 | !plugin.settings.collapsableFileNode 74 | ) 75 | return; 76 | 77 | if ( 78 | node.unknownData.type === "text" || 79 | (node.unknownData.type === "file" && 80 | node.file.extension === "md") 81 | ) { 82 | const content = v.view.state.doc.toString(); 83 | if ( 84 | node.headerComponent && 85 | plugin.settings.minLineAmount > 0 && 86 | content.split("\n").length < 87 | plugin.settings.minLineAmount 88 | ) { 89 | node.headerComponent?.unload(); 90 | node.headerComponent = undefined; 91 | return; 92 | } else if ( 93 | !node.headerComponent && 94 | plugin.settings.minLineAmount > 0 && 95 | content.split("\n").length >= 96 | plugin.settings.minLineAmount 97 | ) { 98 | node.headerComponent = new CollapseControlHeader( 99 | plugin, 100 | node 101 | ); 102 | (node.containerEl as HTMLDivElement).prepend( 103 | node.headerComponent.onload() 104 | ); 105 | 106 | node.headerComponent.updateNode(); 107 | } 108 | } 109 | } 110 | } 111 | }); 112 | }; 113 | 114 | export default class CanvasCollapsePlugin extends Plugin { 115 | triggerByPlugin: boolean = false; 116 | patchSucceed: boolean = false; 117 | 118 | settings: CanvasCollapseSettings; 119 | 120 | headerComponents: { [key: string]: CollapseControlHeader[] } = {}; 121 | 122 | async onload() { 123 | await this.loadSettings(); 124 | this.addSettingTab(new CollapseSettingTab(this.app, this)); 125 | 126 | this.registerCommands(); 127 | this.registerCanvasEvents(); 128 | this.registerCustomIcons(); 129 | 130 | this.registerEditorExtension([DynamicUpdateControlHeader(this)]); 131 | 132 | this.initGlobalCss(); 133 | 134 | this.app.workspace.onLayoutReady(() => { 135 | aroundCanvasMethods(this); 136 | patchCanvasMenu(this); 137 | patchCanvasInteraction(this); 138 | patchCanvasNode(this); 139 | }); 140 | } 141 | 142 | onunload() { 143 | console.log("unloading plugin"); 144 | refreshAllCanvasView(this.app); 145 | } 146 | 147 | registerCommands() { 148 | this.addCommand({ 149 | id: "fold-all-nodes", 150 | name: "Fold all nodes", 151 | checkCallback: (checking: boolean) => 152 | handleNodesViaCommands(this, checking, true, true), 153 | }); 154 | 155 | this.addCommand({ 156 | id: "expand-all-nodes", 157 | name: "Expand all nodes", 158 | checkCallback: (checking: boolean) => 159 | handleNodesViaCommands(this, checking, true, false), 160 | }); 161 | 162 | this.addCommand({ 163 | id: "fold-selected-nodes", 164 | name: "Fold selected nodes", 165 | checkCallback: (checking: boolean) => 166 | handleNodesViaCommands(this, checking, false, true), 167 | }); 168 | 169 | this.addCommand({ 170 | id: "expand-selected-nodes", 171 | name: "Expand selected nodes", 172 | checkCallback: (checking: boolean) => 173 | handleNodesViaCommands(this, checking, false, false), 174 | }); 175 | } 176 | 177 | registerCanvasEvents() { 178 | this.registerEvent( 179 | this.app.workspace.on("collapse-node:patched-canvas", () => { 180 | refreshAllCanvasView(this.app); 181 | }) 182 | ); 183 | this.registerEvent( 184 | this.app.workspace.on("canvas:selection-menu", (menu, canvas) => { 185 | handleSelectionContextMenu(this, menu, canvas); 186 | }) 187 | ); 188 | this.registerEvent( 189 | this.app.workspace.on("canvas:node-menu", (menu, node) => { 190 | handleNodeContextMenu(this, menu, node); 191 | }) 192 | ); 193 | } 194 | 195 | registerCustomIcons() { 196 | addIcon( 197 | "fold-vertical", 198 | `` 199 | ); 200 | addIcon( 201 | "unfold-vertical", 202 | `` 203 | ); 204 | } 205 | 206 | initGlobalCss() { 207 | document.body.toggleClass( 208 | "minimal-control-header", 209 | this.settings?.minimalControlHeader 210 | ); 211 | document.body.toggleClass( 212 | "hide-default-node-title", 213 | this.settings?.hideDefaultNodeTitle 214 | ); 215 | } 216 | 217 | async loadSettings() { 218 | this.settings = Object.assign( 219 | {}, 220 | DEFAULT_SETTINGS, 221 | await this.loadData() 222 | ); 223 | } 224 | 225 | debounceReloadLeaves = debounce(() => { 226 | const leaves = this.app.workspace.getLeavesOfType("canvas"); 227 | leaves.forEach((leaf) => { 228 | leaf.rebuildView(); 229 | }); 230 | }, 1000); 231 | 232 | async saveSettings() { 233 | await this.saveData(this.settings); 234 | 235 | this.debounceReloadLeaves(); 236 | } 237 | } 238 | 239 | export class CollapseSettingTab extends PluginSettingTab { 240 | plugin: CanvasCollapsePlugin; 241 | 242 | constructor(app: any, plugin: CanvasCollapsePlugin) { 243 | super(app, plugin); 244 | this.plugin = plugin; 245 | } 246 | 247 | display(): void { 248 | const { containerEl } = this; 249 | 250 | containerEl.empty(); 251 | 252 | new Setting(containerEl) 253 | .setHeading() 254 | .setName("Enable nodes to be collapsable"); 255 | 256 | this.createCollapsableSetting( 257 | this.plugin, 258 | containerEl, 259 | "File node", 260 | "", 261 | "collapsableFileNode" 262 | ); 263 | this.createCollapsableSetting( 264 | this.plugin, 265 | containerEl, 266 | "Attachment node", 267 | "", 268 | "collapsableAttachmentNode" 269 | ); 270 | this.createCollapsableSetting( 271 | this.plugin, 272 | containerEl, 273 | "Group node", 274 | "", 275 | "collapsableGroupNode" 276 | ); 277 | this.createCollapsableSetting( 278 | this.plugin, 279 | containerEl, 280 | "Link node", 281 | "", 282 | "collapsableLinkNode" 283 | ); 284 | this.createCollapsableSetting( 285 | this.plugin, 286 | containerEl, 287 | "Text node", 288 | "", 289 | "collapsableTextNode" 290 | ); 291 | 292 | new Setting(containerEl).setHeading().setName("Detail settings"); 293 | 294 | new Setting(containerEl) 295 | .setName("Line amount the enable node to be collapsed") 296 | .setDesc( 297 | "The amount of lines that will be shown when the node is collapsed" 298 | ) 299 | .addText((text) => { 300 | text.setValue( 301 | this.plugin.settings.minLineAmount.toString() 302 | ).onChange(async (value) => { 303 | if (!isNaN(Number(value))) { 304 | this.plugin.settings.minLineAmount = Number(value); 305 | await this.plugin.saveSettings(); 306 | } 307 | }); 308 | }); 309 | 310 | new Setting(containerEl) 311 | .setName("Minimal control header") 312 | .setDesc( 313 | "Hide the text and also icon in the control header of the node" 314 | ) 315 | .addToggle((toggle) => { 316 | toggle 317 | .setValue(this.plugin.settings.minimalControlHeader) 318 | .onChange(async (value) => { 319 | this.plugin.settings.minimalControlHeader = value; 320 | document.body.toggleClass( 321 | "minimal-control-header", 322 | value 323 | ); 324 | await this.plugin.saveSettings(); 325 | }); 326 | }); 327 | 328 | new Setting(containerEl) 329 | .setName("Show thumbnails in collapsed state") 330 | .setDesc("Show thumbnails in the collapsed state of the node") 331 | .addToggle((toggle) => { 332 | toggle 333 | .setValue( 334 | this.plugin.settings.showThumbnailsInCollapsedState 335 | ) 336 | .onChange(async (value) => { 337 | this.plugin.settings.showThumbnailsInCollapsedState = 338 | value; 339 | await this.plugin.saveSettings(); 340 | }); 341 | }); 342 | 343 | new Setting(containerEl) 344 | .setName("Show aliases in collapsed state") 345 | .setDesc("Show aliases in the collapsed state of the node") 346 | .addToggle((toggle) => { 347 | toggle 348 | .setValue(this.plugin.settings.showAliasesInCollapsedState) 349 | .onChange(async (value) => { 350 | this.plugin.settings.showAliasesInCollapsedState = 351 | value; 352 | await this.plugin.saveSettings(); 353 | }); 354 | }); 355 | 356 | new Setting(containerEl) 357 | .setName("Hide default node title") 358 | .setDesc("Hide the default title of the node") 359 | .addToggle((toggle) => { 360 | toggle 361 | .setValue(this.plugin.settings.hideDefaultNodeTitle) 362 | .onChange(async (value) => { 363 | this.plugin.settings.hideDefaultNodeTitle = value; 364 | document.body.toggleClass( 365 | "hide-default-node-title", 366 | value 367 | ); 368 | await this.plugin.saveSettings(); 369 | }); 370 | }); 371 | 372 | new Setting(containerEl) 373 | .setName("Show aliases always") 374 | .setDesc( 375 | "Show aliases always in the collapsed/expanded state of the node. Replace current title with alias." 376 | ) 377 | .addToggle((toggle) => { 378 | toggle 379 | .setValue(this.plugin.settings.showAliasesAlways) 380 | .onChange(async (value) => { 381 | this.plugin.settings.showAliasesAlways = value; 382 | await this.plugin.saveSettings(); 383 | }); 384 | }); 385 | 386 | new Setting(containerEl) 387 | .setName("Show thumbnails always") 388 | .setDesc( 389 | "Show thumbnails always in the collapsed state of the node" 390 | ) 391 | .addToggle((toggle) => { 392 | toggle 393 | .setValue(this.plugin.settings.showThumbnailsAlways) 394 | .onChange(async (value) => { 395 | this.plugin.settings.showThumbnailsAlways = value; 396 | await this.plugin.saveSettings(); 397 | }); 398 | }); 399 | } 400 | 401 | createCollapsableSetting( 402 | plugin: CanvasCollapsePlugin, 403 | containerEl: HTMLElement, 404 | name: string, 405 | desc: string, 406 | settingKey: keyof CollapsableNodeSettings 407 | ) { 408 | new Setting(containerEl) 409 | .setName(name) 410 | .setDesc(desc) 411 | .addToggle((toggle) => { 412 | toggle 413 | .setValue(plugin.settings[settingKey]) 414 | .onChange(async (value) => { 415 | plugin.settings[settingKey] = value; 416 | await plugin.saveSettings(); 417 | }); 418 | }); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/patchUtils.ts: -------------------------------------------------------------------------------- 1 | import { around } from "monkey-around"; 2 | import { 3 | Canvas, 4 | CanvasGroupNode, 5 | CanvasNode, 6 | Menu, 7 | setIcon, 8 | setTooltip, 9 | ViewState, 10 | WorkspaceLeaf, 11 | } from "obsidian"; 12 | 13 | import { CanvasCoords } from "obsidian"; 14 | 15 | import { CanvasView } from "obsidian"; 16 | import CanvasCollapsePlugin from "."; 17 | import { CanvasData } from "obsidian/canvas"; 18 | 19 | import { 20 | getSelectionCoords, 21 | handleCanvasMenu, 22 | handleMultiNodesViaNodes, 23 | handleSingleNode, 24 | } from "./utils"; 25 | import CollapseControlHeader from "./ControlHeader"; 26 | 27 | export const aroundCanvasMethods = (plugin: CanvasCollapsePlugin) => { 28 | const patchCanvasMethod = (canvasView: CanvasView) => { 29 | const checkCoords = (e: CanvasCoords, t: CanvasCoords) => { 30 | return ( 31 | e.minX <= t.minX && 32 | e.minY <= t.minY && 33 | e.maxX >= t.maxX && 34 | e.maxY >= t.maxY 35 | ); 36 | }; 37 | 38 | const checkTriggerByPlugin = () => { 39 | return plugin.triggerByPlugin; 40 | }; 41 | 42 | const toggleTriggerByPlugin = () => { 43 | plugin.triggerByPlugin = !plugin.triggerByPlugin; 44 | }; 45 | 46 | const patchCanvas = (canvas: Canvas) => { 47 | const uninstaller = around(canvas.constructor.prototype, { 48 | getContainingNodes: (next: any) => 49 | function (e: CanvasCoords) { 50 | const result = next.call(this, e); 51 | 52 | const checkExistGroupNode: CanvasNode | null = 53 | this.nodeIndex 54 | .search(e) 55 | .find( 56 | (t: CanvasNode) => 57 | t.unknownData.type === "group" || 58 | (t as CanvasGroupNode).label 59 | ); 60 | if (!checkExistGroupNode) return result; 61 | const renewCoords = checkExistGroupNode?.getBBox(true); 62 | if (renewCoords !== e && e.maxY - e.minY === 40) { 63 | const newResult = this.nodeIndex 64 | .search(renewCoords) 65 | .filter((t: any) => { 66 | return checkCoords( 67 | renewCoords, 68 | t.getBBox(true) 69 | ); 70 | }); 71 | if (newResult.length > result.length) { 72 | return newResult; 73 | } 74 | } 75 | return result; 76 | }, 77 | requestSave: (next: any) => 78 | function (args?: boolean, triggerBySelf?: boolean) { 79 | next.call(this, args); 80 | if (triggerBySelf) { 81 | if (args !== undefined) { 82 | this.data = this.getData(); 83 | args && this.requestPushHistory(this.data); 84 | } 85 | } 86 | }, 87 | pushHistory: (next: any) => 88 | function (args: CanvasData) { 89 | if (checkTriggerByPlugin()) { 90 | toggleTriggerByPlugin(); 91 | return; 92 | } 93 | return next.call(this, args); 94 | }, 95 | selectAll: (next: any) => 96 | function (e: Set) { 97 | if (this.wrapperEl.querySelector(".canvas-selection")) { 98 | const domCoords = getSelectionCoords( 99 | this.wrapperEl.querySelector( 100 | ".canvas-selection" 101 | ) as HTMLElement 102 | ); 103 | if (domCoords) { 104 | const newResult = Array.from(e).filter( 105 | (t: CanvasNode) => { 106 | if (!t.unknownData.collapsed) 107 | return true; 108 | if ( 109 | t.nodeEl.hasClass( 110 | "group-nodes-collapsed" 111 | ) 112 | ) 113 | return false; 114 | return checkCoords( 115 | domCoords, 116 | t.getBBox() 117 | ); 118 | } 119 | ); 120 | if (newResult.length > 0) { 121 | const ne = new Set(newResult); 122 | return next.call(this, ne); 123 | } 124 | if (newResult.length === 0) { 125 | return; 126 | } 127 | } 128 | } 129 | return next.call(this, e); 130 | }, 131 | createTextNode: (next: any) => 132 | function (args: any) { 133 | if (args.size === undefined && args.pos) { 134 | return next.call(this, { 135 | ...args, 136 | pos: { 137 | x: args.pos.x, 138 | y: args.pos.y, 139 | width: args?.size?.width || 250, 140 | height: args?.size?.height || 140, 141 | }, 142 | size: { 143 | x: args.pos.x, 144 | y: args.pos.y, 145 | width: args?.size?.width || 250, 146 | height: args?.size?.height || 140, 147 | }, 148 | }); 149 | } 150 | return next.call(this, args); 151 | }, 152 | createGroupNode: (next: any) => 153 | function (args: any) { 154 | if (args.size !== undefined && args.pos) { 155 | return next.call(this, { 156 | ...args, 157 | pos: { 158 | x: args.pos.x, 159 | y: args.pos.y - 30, 160 | width: args?.size?.width, 161 | height: args?.size?.height + 30, 162 | }, 163 | size: { 164 | x: args.pos.x, 165 | y: args.pos.y - 30, 166 | width: args?.size?.width, 167 | height: args?.size?.height + 30, 168 | }, 169 | }); 170 | } 171 | return next.call(this, args); 172 | }, 173 | }); 174 | plugin.register(uninstaller); 175 | plugin.patchSucceed = true; 176 | 177 | console.log("Obsidian-Collapse-Node: canvas patched"); 178 | return true; 179 | }; 180 | 181 | patchCanvas(canvasView.canvas); 182 | }; 183 | 184 | const uninstaller = around(WorkspaceLeaf.prototype, { 185 | setViewState(next) { 186 | return function (state: ViewState, ...rest: any[]) { 187 | if (state.state?.file) { 188 | if ( 189 | state.state?.file && 190 | (state.state?.file as string).endsWith(".canvas") 191 | ) { 192 | setTimeout(() => { 193 | if (this.view.canvas) { 194 | patchCanvasMethod(this.view); 195 | uninstaller(); 196 | } 197 | }, 400); 198 | } 199 | } 200 | 201 | return next.apply(this, [state, ...rest]); 202 | }; 203 | }, 204 | }); 205 | plugin.register(uninstaller); 206 | }; 207 | 208 | export const patchCanvasMenu = (plugin: CanvasCollapsePlugin) => { 209 | const triggerPlugin = () => { 210 | plugin.triggerByPlugin = true; 211 | }; 212 | 213 | const patchMenu = async () => { 214 | const canvasLeaf = plugin.app.workspace 215 | .getLeavesOfType("canvas") 216 | .first(); 217 | 218 | if (canvasLeaf?.isDeferred) { 219 | await canvasLeaf.loadIfDeferred(); 220 | } 221 | 222 | const canvasView = canvasLeaf?.view; 223 | 224 | if (!canvasView) return false; 225 | const menu = (canvasView as CanvasView)?.canvas.menu; 226 | if (!menu) return false; 227 | 228 | const selection = menu.selection; 229 | if (!selection) return false; 230 | 231 | const menuUninstaller = around(menu.constructor.prototype, { 232 | render: (next: any) => 233 | function (...args: any) { 234 | const result = next.call(this, ...args); 235 | if (this.menuEl.querySelector(".collapse-node-menu-item")) 236 | return result; 237 | const buttonEl = createEl( 238 | "button", 239 | "clickable-icon collapse-node-menu-item" 240 | ); 241 | setTooltip(buttonEl, "Fold selected nodes", { 242 | placement: "top", 243 | }); 244 | setIcon(buttonEl, "lucide-chevrons-left-right"); 245 | this.menuEl.appendChild(buttonEl); 246 | buttonEl.addEventListener("click", () => { 247 | const pos = buttonEl.getBoundingClientRect(); 248 | if (!buttonEl.hasClass("has-active-menu")) { 249 | buttonEl.toggleClass("has-active-menu", true); 250 | const menu = new Menu(); 251 | const containingNodes = 252 | this.canvas.getContainingNodes( 253 | this.selection.bbox 254 | ); 255 | 256 | handleCanvasMenu(menu, async (isFold: boolean) => { 257 | triggerPlugin(); 258 | const currentSelection = this.canvas.selection; 259 | containingNodes.length > 1 260 | ? handleMultiNodesViaNodes( 261 | this.canvas, 262 | containingNodes, 263 | isFold 264 | ) 265 | : currentSelection 266 | ? handleSingleNode( 267 | ( 268 | Array.from( 269 | currentSelection 270 | )?.first() 271 | ), 272 | isFold 273 | ) 274 | : ""; 275 | buttonEl.toggleClass("has-active-menu", false); 276 | }); 277 | menu.setParentElement(this.menuEl).showAtPosition({ 278 | x: pos.x, 279 | y: pos.bottom, 280 | width: pos.width, 281 | overlap: true, 282 | }); 283 | } 284 | }); 285 | 286 | return result; 287 | }, 288 | }); 289 | 290 | plugin.register(menuUninstaller); 291 | plugin.app.workspace.trigger("collapse-node:patched-canvas"); 292 | 293 | console.log("Obsidian-Collapse-Node: canvas history patched"); 294 | return true; 295 | }; 296 | 297 | plugin.app.workspace.onLayoutReady(async () => { 298 | if (!(await patchMenu())) { 299 | const evt = plugin.app.workspace.on("layout-change", async () => { 300 | (await patchMenu()) && plugin.app.workspace.offref(evt); 301 | }); 302 | plugin.registerEvent(evt); 303 | } 304 | }); 305 | }; 306 | 307 | export const patchCanvasInteraction = (plugin: CanvasCollapsePlugin) => { 308 | const patchInteraction = () => { 309 | const canvasView = plugin.app.workspace 310 | .getLeavesOfType("canvas") 311 | .first()?.view; 312 | if (!canvasView) return false; 313 | 314 | const canvas = (canvasView as CanvasView)?.canvas.nodeInteractionLayer; 315 | if (!canvas) return false; 316 | 317 | const uninstaller = around(canvas.constructor.prototype, { 318 | render: (next: any) => 319 | function (...args: any) { 320 | const result = next.call(this, ...args); 321 | if (!this.target) return result; 322 | const isCollapsed = 323 | this.target.nodeEl.hasClass("collapsed"); 324 | const isGroupNodesCollapsed = this.target.nodeEl.hasClass( 325 | "group-nodes-collapsed" 326 | ); 327 | 328 | if (this.target.unknownData) { 329 | this.interactionEl.toggleClass( 330 | "collapsed-interaction", 331 | isCollapsed 332 | ); 333 | } 334 | this.interactionEl.toggleClass( 335 | "group-nodes-collapsed", 336 | isGroupNodesCollapsed 337 | ); 338 | return result; 339 | }, 340 | }); 341 | plugin.register(uninstaller); 342 | 343 | console.log("Obsidian-Collapse-Node: canvas history patched"); 344 | return true; 345 | }; 346 | 347 | plugin.app.workspace.onLayoutReady(() => { 348 | if (!patchInteraction()) { 349 | const evt = plugin.app.workspace.on("layout-change", () => { 350 | patchInteraction() && plugin.app.workspace.offref(evt); 351 | }); 352 | plugin.registerEvent(evt); 353 | } 354 | }); 355 | }; 356 | 357 | export const initControlHeader = (plugin: CanvasCollapsePlugin, node: any) => { 358 | return new CollapseControlHeader(plugin, node); 359 | }; 360 | 361 | export const renderNodeWithHeader = ( 362 | plugin: CanvasCollapsePlugin, 363 | node: any 364 | ) => { 365 | // If header already exists, don't create another one 366 | if (node.headerComponent) return; 367 | 368 | // Check node type against plugin settings 369 | const nodeType = node.unknownData.type; 370 | 371 | // Return early if this node type is disabled in settings 372 | if ( 373 | nodeType === "file" && 374 | node.file?.extension === "md" && 375 | !plugin.settings.collapsableFileNode 376 | ) 377 | return; 378 | if ( 379 | nodeType === "file" && 380 | node.file?.extension !== "md" && 381 | !plugin.settings.collapsableAttachmentNode 382 | ) 383 | return; 384 | if (nodeType === "group" && !plugin.settings.collapsableGroupNode) return; 385 | if (nodeType === "link" && !plugin.settings.collapsableLinkNode) return; 386 | if (nodeType === "text" && !plugin.settings.collapsableTextNode) return; 387 | 388 | // Check minimum line amount for text and file nodes 389 | if ( 390 | plugin.settings.minLineAmount > 0 && 391 | (nodeType === "text" || nodeType === "file") 392 | ) { 393 | let lineCount = 0; 394 | 395 | if (nodeType === "text" && typeof node.text === "string") { 396 | lineCount = node.text.split("\n").length; 397 | } else if ( 398 | nodeType === "file" && 399 | node.file?.extension === "md" && 400 | node.child 401 | ) { 402 | lineCount = node.child.data?.split("\n").length || 0; 403 | } 404 | 405 | if (lineCount < plugin.settings.minLineAmount) return; 406 | } 407 | 408 | // Create header component 409 | node.headerComponent = initControlHeader( 410 | plugin, 411 | node 412 | ) as CollapseControlHeader; 413 | node.nodeEl.setAttribute("data-node-type", nodeType); 414 | 415 | // Wait for containerEl to be loaded before adding header 416 | const addHeader = () => { 417 | if (!node.containerEl) { 418 | setTimeout(addHeader, 0); 419 | return; 420 | } 421 | (node.containerEl as HTMLDivElement).prepend( 422 | node.headerComponent.onload() 423 | ); 424 | }; 425 | addHeader(); 426 | 427 | // Apply collapsed state if needed 428 | if (node.unknownData.collapsed) { 429 | node.nodeEl.toggleClass("collapsed", true); 430 | node.headerComponent.updateEdges(); 431 | } 432 | }; 433 | 434 | export const updateAllNodeWithHeader = (plugin: CanvasCollapsePlugin) => { 435 | const canvasLeaves = plugin.app.workspace.getLeavesOfType("canvas"); 436 | 437 | for (const canvasLeaf of canvasLeaves) { 438 | const canvas: Canvas = (canvasLeaf.view as CanvasView)?.canvas; 439 | if (!canvas) continue; 440 | 441 | const nodes = canvas.nodes.values(); 442 | 443 | for (const node of nodes) { 444 | renderNodeWithHeader(plugin, node); 445 | } 446 | } 447 | }; 448 | 449 | export const patchCanvasNode = (plugin: CanvasCollapsePlugin) => { 450 | const patchNode = () => { 451 | const canvasView = plugin.app.workspace 452 | .getLeavesOfType("canvas") 453 | .first()?.view; 454 | if (!canvasView) return false; 455 | 456 | const canvas: Canvas = (canvasView as CanvasView)?.canvas; 457 | if (!canvas) return false; 458 | 459 | const node = ( 460 | plugin.app.workspace.getLeavesOfType("canvas").first()?.view as any 461 | ).canvas.nodes 462 | .values() 463 | .next().value; 464 | 465 | if (!node) return false; 466 | let prototype = Object.getPrototypeOf(node); 467 | while (prototype && prototype !== Object.prototype) { 468 | prototype = Object.getPrototypeOf(prototype); 469 | // @ts-expected-error Find the parent prototype 470 | if (prototype.renderZIndex) { 471 | break; 472 | } 473 | } 474 | 475 | console.log(prototype); 476 | 477 | if (!prototype) return false; 478 | 479 | const uninstaller = around(prototype, { 480 | render: (next: any) => 481 | function (...args: any) { 482 | const result = next.call(this, ...args); 483 | renderNodeWithHeader(plugin, this); 484 | return result; 485 | }, 486 | getBBox: (next: any) => 487 | function (containing?: boolean) { 488 | const result = next.call(this); 489 | if ( 490 | containing !== true && 491 | (this.nodeEl as HTMLDivElement).hasClass("collapsed") 492 | ) { 493 | const x = this.x; 494 | const y = this.y; 495 | const width = this.width; 496 | const height = 40; 497 | return { 498 | minX: x, 499 | minY: y, 500 | maxX: x + width, 501 | maxY: y + height, 502 | }; 503 | } 504 | return result; 505 | }, 506 | setData: (next: any) => 507 | function (data: any) { 508 | if (data.collapsed !== undefined) { 509 | this.headerComponent?.setCollapsed(data.collapsed); 510 | } 511 | return next.call(this, data); 512 | }, 513 | }); 514 | plugin.register(uninstaller); 515 | updateAllNodeWithHeader(plugin); 516 | 517 | console.log("Obsidian-Collapse-Node: canvas node patched"); 518 | return true; 519 | }; 520 | 521 | plugin.app.workspace.onLayoutReady(() => { 522 | if (!patchNode()) { 523 | const evt = plugin.app.workspace.on("layout-change", () => { 524 | patchNode() && plugin.app.workspace.offref(evt); 525 | }); 526 | plugin.registerEvent(evt); 527 | } 528 | }); 529 | }; 530 | -------------------------------------------------------------------------------- /src/types/canvas.d.ts: -------------------------------------------------------------------------------- 1 | export type CanvasNodeType = 'link' | 'file' | 'text' | 'group'; 2 | export type CanvasDirection = 3 | 'bottomright' 4 | | 'bottomleft' 5 | | 'topright' 6 | | 'topleft' 7 | | 'right' 8 | | 'left' 9 | | 'top' 10 | | 'bottom'; 11 | 12 | export interface CanvasNodeUnknownData { 13 | id: string; 14 | type: CanvasNodeType; 15 | collapsed: boolean; 16 | 17 | [key: string]: any; 18 | } 19 | -------------------------------------------------------------------------------- /src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | import { CanvasNode, Component } from "obsidian"; 2 | import { CanvasNodeType } from "./canvas"; 3 | 4 | interface HeaderComponent extends Component { 5 | checkNodeType(): CanvasNodeType; 6 | 7 | initHeader(): void; 8 | 9 | initContent(): void; 10 | 11 | initTypeIcon(): void; 12 | 13 | setIconOrContent(action: string): void; 14 | 15 | setCollapsed(collapsed: boolean): void; 16 | 17 | toggleCollapsed(): Promise; 18 | 19 | refreshHistory(): void; 20 | 21 | updateNode(): void; 22 | 23 | updateNodesInGroup(): void; 24 | 25 | updateEdges(): void; 26 | 27 | updateEdgesInGroup(node: CanvasNode, triggerCollapsed?: boolean): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/types/event.d.ts: -------------------------------------------------------------------------------- 1 | type CanvasCollapseEvt = "collapse" | "expand"; 2 | 3 | -------------------------------------------------------------------------------- /src/types/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | import { App, Component, EventRef, ItemView, TFile } from "obsidian"; 3 | import { CanvasDirection, CanvasNodeUnknownData } from "./canvas"; 4 | import { CanvasData } from "obsidian/canvas"; 5 | 6 | declare module "obsidian" { 7 | 8 | type CanvasNodeID = string; 9 | type CanvasEdgeID = string; 10 | 11 | interface Menu { 12 | setParentElement(parent: HTMLElement): Menu; 13 | } 14 | 15 | interface MenuItem { 16 | setSubmenu(): Menu; 17 | } 18 | 19 | interface WorkspaceLeaf { 20 | rebuildView(): void; 21 | } 22 | 23 | interface Workspace { 24 | on( 25 | name: CanvasCollapseEvt, 26 | cb: ( 27 | view: ItemView, 28 | nodeID?: string[] | undefined, 29 | ) => any, 30 | ): EventRef; 31 | 32 | on( 33 | name: "canvas:selection-menu", 34 | cb: ( 35 | menu: Menu, 36 | canvas: Canvas, 37 | ) => any, 38 | ): EventRef; 39 | 40 | on( 41 | name: "canvas:node-menu", 42 | cb: ( 43 | menu: Menu, 44 | node: CanvasNode, 45 | ) => any, 46 | ): EventRef; 47 | 48 | on( 49 | name: "collapse-node:plugin-disabled", 50 | cb: () => any, 51 | ): EventRef; 52 | 53 | on( 54 | name: "collapse-node:patched-canvas", 55 | cb: () => any, 56 | ): EventRef; 57 | } 58 | 59 | interface Workspace { 60 | trigger( 61 | name: CanvasCollapseEvt, 62 | view: ItemView, 63 | nodeID?: string[] | undefined, 64 | ): void; 65 | 66 | trigger( 67 | name: "collapse-node:plugin-disabled", 68 | ): void; 69 | 70 | trigger( 71 | name: "collapse-node:patched-canvas", 72 | ): void; 73 | } 74 | 75 | interface CanvasView extends View { 76 | canvas: Canvas; 77 | } 78 | 79 | interface Canvas { 80 | readonly: boolean; 81 | 82 | x: number; 83 | y: number; 84 | nodes: Map; 85 | edges: Map; 86 | nodeInteractionLayer: CanvasInteractionLayer; 87 | selection: Set; 88 | 89 | menu: CanvasMenu; 90 | 91 | wrapperEl: HTMLElement; 92 | 93 | history: any; 94 | requestPushHistory: any; 95 | nodeIndex: any; 96 | 97 | requestSave(save?: boolean, triggerBySelf?: boolean): void; 98 | 99 | getData(): CanvasData; 100 | 101 | setData(data: CanvasData): void; 102 | 103 | getEdgesForNode(node: CanvasNode): CanvasEdge[]; 104 | 105 | getContainingNodes(coords: CanvasCoords): CanvasNode[]; 106 | 107 | deselectAll(): void; 108 | 109 | select(nodes: CanvasNode): void; 110 | 111 | requestFrame(): void; 112 | } 113 | 114 | interface ICanvasData { 115 | nodes: CanvasNode[]; 116 | edges: CanvasEdge[]; 117 | } 118 | 119 | interface CanvasMenu { 120 | containerEl: HTMLElement; 121 | menuEl: HTMLElement; 122 | canvas: Canvas; 123 | selection: CanvasSelection; 124 | 125 | render(): void; 126 | 127 | updateZIndex(): void; 128 | } 129 | 130 | interface CanvasSelection { 131 | selectionEl: HTMLElement; 132 | resizerEls: HTMLElement; 133 | canvas: Canvas; 134 | bbox: CanvasCoords | undefined; 135 | 136 | render(): void; 137 | 138 | hide(): void; 139 | 140 | onResizePointerDown(e: PointerEvent, direction: CanvasDirection): void; 141 | 142 | update(bbox: CanvasCoords): void; 143 | 144 | } 145 | 146 | interface CanvasInteractionLayer { 147 | interactionEl: HTMLElement; 148 | canvas: Canvas; 149 | target: CanvasNode | null; 150 | 151 | render(): void; 152 | 153 | setTarget(target: CanvasNode | null): void; 154 | } 155 | 156 | interface CanvasNode { 157 | id: CanvasNodeID; 158 | 159 | x: number; 160 | y: number; 161 | width: number; 162 | height: number; 163 | zIndex: number; 164 | bbox: CanvasCoords; 165 | unknownData: CanvasNodeUnknownData; 166 | renderedZIndex: number; 167 | 168 | headerComponent: Component; 169 | 170 | nodeEl: HTMLElement; 171 | labelEl: HTMLElement; 172 | contentEl: HTMLElement; 173 | containerEl: HTMLElement; 174 | 175 | canvas: Canvas; 176 | app: App; 177 | 178 | getBBox(containing?: boolean): CanvasCoords; 179 | 180 | render(): void; 181 | } 182 | 183 | interface CanvasTextNode extends CanvasNode { 184 | text: string; 185 | } 186 | 187 | interface CanvasFileNode extends CanvasNode { 188 | file: TFile; 189 | } 190 | 191 | interface CanvasLinkNode extends CanvasNode { 192 | url: string; 193 | } 194 | 195 | interface CanvasGroupNode extends CanvasNode { 196 | label: string; 197 | } 198 | 199 | interface CanvasEdge { 200 | id: CanvasEdgeID; 201 | 202 | label: string | undefined; 203 | lineStartGroupEl: SVGGElement; 204 | lineEndGroupEl: SVGGElement; 205 | lineGroupEl: SVGGElement; 206 | 207 | labelElement: { 208 | wrapperEl: HTMLElement; 209 | textEl: HTMLElement; 210 | }; 211 | 212 | path: { 213 | display: SVGPathElement; 214 | interaction: SVGPathElement; 215 | } 216 | 217 | canvas: Canvas; 218 | bbox: CanvasCoords; 219 | render(): void; 220 | 221 | unknownData: CanvasNodeUnknownData; 222 | } 223 | 224 | interface CanvasCoords { 225 | maxX: number; 226 | maxY: number; 227 | minX: number; 228 | minY: number; 229 | } 230 | } 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Canvas, 4 | CanvasCoords, 5 | CanvasNode, 6 | ItemView, 7 | Menu, 8 | MenuItem, 9 | Modal, 10 | SuggestModal, 11 | TFile, 12 | } from "obsidian"; 13 | import CollapseControlHeader from "./ControlHeader"; 14 | import CanvasCollapsePlugin from "."; 15 | 16 | const getBoundingRect = (nodes: CanvasNode[]) => { 17 | const bboxArray = nodes.map((t: CanvasNode) => t.getBBox()); 18 | 19 | const minX = Math.min(...bboxArray.map((t: CanvasCoords) => t.minX)); 20 | const minY = Math.min(...bboxArray.map((t: CanvasCoords) => t.minY)); 21 | const maxX = Math.max(...bboxArray.map((t: CanvasCoords) => t.maxX)); 22 | const maxY = Math.max(...bboxArray.map((t: CanvasCoords) => t.maxY)); 23 | 24 | return { 25 | minX, 26 | minY, 27 | maxX, 28 | maxY, 29 | }; 30 | }; 31 | const updateSelection = (canvas: Canvas) => { 32 | if (canvas.menu.selection.bbox) { 33 | const selection = Array.from(canvas.selection); 34 | const currentNodesInSelection = canvas.getContainingNodes( 35 | canvas.menu.selection.bbox 36 | ); 37 | if (currentNodesInSelection.length > 0) { 38 | const boundingRect = getBoundingRect( 39 | selection.length > currentNodesInSelection.length 40 | ? selection 41 | : currentNodesInSelection 42 | ); 43 | if (boundingRect) { 44 | canvas.menu.selection.update(boundingRect); 45 | } 46 | } 47 | } 48 | }; 49 | const handleMultiNodes = ( 50 | canvas: Canvas, 51 | allNodes: boolean, 52 | collapse: boolean 53 | ) => { 54 | const nodes = allNodes 55 | ? Array.from(canvas.nodes.values()) 56 | : (Array.from(canvas.selection) as any[]); 57 | const canvasData = canvas.getData(); 58 | 59 | if (nodes && nodes.length > 0) { 60 | for (const node of nodes) { 61 | if (node.unknownData.type === "group") { 62 | node.headerComponent.updateNodesInGroup(collapse); 63 | } 64 | node.headerComponent?.setCollapsed(collapse); 65 | const nodeData = canvasData.nodes.find( 66 | (t: any) => t.id === node.id 67 | ); 68 | if (nodeData) nodeData.collapsed = collapse; 69 | } 70 | canvas.setData(canvasData); 71 | } 72 | canvas.requestSave(true, true); 73 | canvas.requestFrame(); 74 | updateSelection(canvas); 75 | }; 76 | export const handleMultiNodesViaNodes = ( 77 | canvas: Canvas, 78 | nodes: CanvasNode[], 79 | collapse: boolean 80 | ) => { 81 | const canvasData = canvas.getData(); 82 | 83 | if (nodes && nodes.length > 0) { 84 | for (const node of nodes) { 85 | if (node.unknownData.type === "group") { 86 | ( 87 | node.headerComponent as CollapseControlHeader 88 | ).updateNodesInGroup(collapse); 89 | } 90 | (node.headerComponent as CollapseControlHeader)?.setCollapsed( 91 | collapse 92 | ); 93 | const nodeData = canvasData.nodes.find( 94 | (t: any) => t.id === node.id 95 | ); 96 | if (nodeData) nodeData.collapsed = collapse; 97 | } 98 | canvas.setData(canvasData); 99 | } 100 | canvas.requestSave(true, true); 101 | updateSelection(canvas); 102 | }; 103 | export const handleSingleNode = (node: CanvasNode, collapse: boolean) => { 104 | if (node.unknownData.type === "group") { 105 | (node.headerComponent as CollapseControlHeader).updateNodesInGroup(); 106 | } 107 | const canvasData = node.canvas.getData(); 108 | const nodeData = canvasData.nodes.find((t: any) => t.id === node.id); 109 | if (nodeData) nodeData.collapsed = collapse; 110 | node.canvas.setData(canvasData); 111 | node.canvas.requestSave(true, true); 112 | updateSelection(node.canvas); 113 | }; 114 | 115 | export const handleNodesViaCommands = ( 116 | plugin: CanvasCollapsePlugin, 117 | checking: boolean, 118 | allNodes: boolean, 119 | collapse: boolean 120 | ) => { 121 | plugin.triggerByPlugin = true; 122 | const currentView = plugin.app.workspace.getActiveViewOfType(ItemView); 123 | if (currentView && currentView.getViewType() === "canvas") { 124 | if (!checking) { 125 | const canvasView = currentView as any; 126 | const canvas = canvasView.canvas as Canvas; 127 | handleMultiNodes(canvas, allNodes, collapse); 128 | } 129 | 130 | return true; 131 | } 132 | }; 133 | 134 | const createHandleContextMenu = ( 135 | section: string, 136 | callback: (isFold: boolean) => Promise 137 | ) => { 138 | return (menu: Menu) => { 139 | menu.addItem((item: MenuItem) => { 140 | const subMenu = item 141 | .setSection(section) 142 | .setTitle("Collapse node") 143 | .setIcon("chevrons-left-right") 144 | .setSubmenu(); 145 | handleCanvasMenu(subMenu, callback); 146 | }); 147 | }; 148 | }; 149 | 150 | export const handleCanvasMenu = ( 151 | subMenu: Menu, 152 | callback: (isFold: boolean) => Promise 153 | ) => { 154 | return subMenu 155 | .addItem((item: MenuItem) => { 156 | item.setIcon("fold-vertical") 157 | .setTitle("Fold selected nodes") 158 | .onClick(async () => { 159 | await callback(true); 160 | }); 161 | }) 162 | .addItem((item: any) => { 163 | item.setIcon("unfold-vertical") 164 | .setTitle("Expand selected nodes") 165 | .onClick(async () => { 166 | await callback(false); 167 | }); 168 | }); 169 | }; 170 | 171 | export const handleSelectionContextMenu = ( 172 | plugin: CanvasCollapsePlugin, 173 | menu: Menu, 174 | canvas: Canvas 175 | ) => { 176 | plugin.triggerByPlugin = true; 177 | const callback = async (isFold: boolean) => { 178 | handleMultiNodes(canvas, false, isFold); 179 | }; 180 | createHandleContextMenu("action", callback)(menu); 181 | }; 182 | 183 | export const handleNodeContextMenu = ( 184 | plugin: CanvasCollapsePlugin, 185 | menu: Menu, 186 | node: CanvasNode 187 | ) => { 188 | plugin.triggerByPlugin = true; 189 | const callback = async (isFold: boolean) => { 190 | handleSingleNode(node, isFold); 191 | }; 192 | createHandleContextMenu("canvas", callback)(menu); 193 | 194 | // Add Alias and Thumbnail menu items 195 | menu.addItem((item: MenuItem) => { 196 | item.setSection("canvas") 197 | .setTitle("Set Node Alias") 198 | .setIcon("text-cursor-input") 199 | .onClick(async () => { 200 | await setNodeAlias(plugin, node); 201 | }); 202 | }); 203 | 204 | menu.addItem((item: MenuItem) => { 205 | item.setSection("canvas") 206 | .setTitle("Set Node Thumbnail") 207 | .setIcon("image") 208 | .onClick(async () => { 209 | await setNodeThumbnail(plugin, node); 210 | }); 211 | }); 212 | 213 | // Add option to remove alias/thumbnail 214 | if (node.unknownData.alias || node.unknownData.thumbnail) { 215 | menu.addItem((item: MenuItem) => { 216 | item.setSection("canvas") 217 | .setTitle("Remove Node Customizations") 218 | .setIcon("trash") 219 | .onClick(async () => { 220 | await removeNodeCustomizations(plugin, node); 221 | }); 222 | }); 223 | } 224 | }; 225 | 226 | // Function to set alias for a node 227 | export const setNodeAlias = async ( 228 | plugin: CanvasCollapsePlugin, 229 | node: CanvasNode 230 | ) => { 231 | const modal = new TextInputModal( 232 | plugin.app, 233 | "Enter alias for node", 234 | node.unknownData.alias || "", 235 | "Set Alias" 236 | ); 237 | 238 | const alias = await modal.openAndGetValue(); 239 | if (alias !== null) { 240 | // Set the alias in node data 241 | node.unknownData.alias = alias; 242 | 243 | // Update the node if it has a header component 244 | if ( 245 | node.headerComponent && 246 | node.headerComponent instanceof CollapseControlHeader 247 | ) { 248 | const header = node.headerComponent as CollapseControlHeader; 249 | header.updateNodeAlias(alias); 250 | header.updateNode(); 251 | } 252 | 253 | // Save the canvas state 254 | node.canvas.requestSave(false, true); 255 | } 256 | }; 257 | 258 | // Function to set thumbnail for a node 259 | export const setNodeThumbnail = async ( 260 | plugin: CanvasCollapsePlugin, 261 | node: CanvasNode 262 | ) => { 263 | const modal = new ThumbnailSelectionModal(plugin.app); 264 | const thumbnailPath = await modal.openAndGetValue(); 265 | 266 | if (thumbnailPath) { 267 | // Set the thumbnail in node data 268 | node.unknownData.thumbnail = thumbnailPath; 269 | 270 | // Update the node if it has a header component 271 | if ( 272 | node.headerComponent && 273 | node.headerComponent instanceof CollapseControlHeader 274 | ) { 275 | const header = node.headerComponent as CollapseControlHeader; 276 | header.updateNodeThumbnail(thumbnailPath); 277 | header.updateNode(); 278 | } 279 | 280 | // Save the canvas state 281 | node.canvas.requestSave(false, true); 282 | } 283 | }; 284 | 285 | // Function to remove alias and thumbnail from node 286 | export const removeNodeCustomizations = async ( 287 | plugin: CanvasCollapsePlugin, 288 | node: CanvasNode 289 | ) => { 290 | delete node.unknownData.alias; 291 | delete node.unknownData.thumbnail; 292 | 293 | // Update the node if it has a header component 294 | if ( 295 | node.headerComponent && 296 | node.headerComponent instanceof CollapseControlHeader 297 | ) { 298 | const header = node.headerComponent as CollapseControlHeader; 299 | header.updateNodeAlias(""); 300 | header.updateNodeThumbnail(""); 301 | header.updateNode(); 302 | } 303 | 304 | // Save the canvas state 305 | node.canvas.requestSave(false, true); 306 | }; 307 | 308 | // Class for text input modal 309 | class TextInputModal extends Modal { 310 | private value: string; 311 | private inputEl: HTMLInputElement; 312 | private resolvePromise: (value: string | null) => void; 313 | private buttonText: string; 314 | 315 | constructor( 316 | app: App, 317 | private title: string, 318 | initialValue: string, 319 | buttonText: string 320 | ) { 321 | super(app); 322 | this.value = initialValue; 323 | this.buttonText = buttonText; 324 | } 325 | 326 | onOpen() { 327 | const { contentEl } = this; 328 | 329 | contentEl.createEl("h2", { text: this.title }); 330 | 331 | this.inputEl = contentEl.createEl("input", { 332 | type: "text", 333 | value: this.value, 334 | }); 335 | this.inputEl.style.width = "100%"; 336 | this.inputEl.style.marginBottom = "1em"; 337 | 338 | // Focus input 339 | setTimeout(() => this.inputEl.focus(), 10); 340 | 341 | // Add buttons 342 | const buttonContainer = contentEl.createDiv(); 343 | buttonContainer.style.display = "flex"; 344 | buttonContainer.style.justifyContent = "flex-end"; 345 | buttonContainer.style.gap = "10px"; 346 | 347 | const cancelButton = buttonContainer.createEl("button", { 348 | text: "Cancel", 349 | }); 350 | const saveButton = buttonContainer.createEl("button", { 351 | text: this.buttonText, 352 | cls: "mod-cta", 353 | }); 354 | 355 | cancelButton.addEventListener("click", () => { 356 | this.resolvePromise(null); 357 | this.close(); 358 | }); 359 | 360 | saveButton.addEventListener("click", () => { 361 | this.resolvePromise(this.inputEl.value); 362 | this.close(); 363 | }); 364 | 365 | // Handle Enter key 366 | this.inputEl.addEventListener("keydown", (e) => { 367 | if (e.key === "Enter") { 368 | this.resolvePromise(this.inputEl.value); 369 | this.close(); 370 | } 371 | }); 372 | } 373 | 374 | onClose() { 375 | const { contentEl } = this; 376 | contentEl.empty(); 377 | } 378 | 379 | openAndGetValue(): Promise { 380 | return new Promise((resolve) => { 381 | this.resolvePromise = resolve; 382 | this.open(); 383 | }); 384 | } 385 | } 386 | 387 | // Class for thumbnail selection modal 388 | class ThumbnailSelectionModal extends Modal { 389 | private resolvePromise: (value: string | null) => void; 390 | private inputEl: HTMLInputElement; 391 | private fileInput: HTMLInputElement; 392 | private preview: HTMLImageElement; 393 | 394 | constructor(app: App) { 395 | super(app); 396 | } 397 | 398 | onOpen() { 399 | const { contentEl } = this; 400 | 401 | contentEl.createEl("h2", { text: "Set Node Thumbnail" }); 402 | 403 | // URL input 404 | contentEl.createEl("p", { 405 | text: "Enter image URL or path to an attachment:", 406 | }); 407 | 408 | this.inputEl = contentEl.createEl("input", { 409 | type: "text", 410 | placeholder: "https://example.com/image.jpg or image.jpg", 411 | }); 412 | this.inputEl.style.width = "100%"; 413 | this.inputEl.style.marginBottom = "1em"; 414 | 415 | // Or use file picker to select from vault 416 | contentEl.createEl("p", { 417 | text: "Or select an image from your attachments:", 418 | }); 419 | 420 | const selectFileButton = contentEl.createEl("button", { 421 | text: "Browse vault files", 422 | }); 423 | selectFileButton.addEventListener("click", () => { 424 | this.openFilePicker(); 425 | }); 426 | 427 | // Preview area 428 | contentEl.createEl("p", { 429 | text: "Preview:", 430 | cls: "thumbnail-preview-label", 431 | }); 432 | this.preview = contentEl.createEl("img", { cls: "thumbnail-preview" }); 433 | this.preview.style.maxWidth = "100%"; 434 | this.preview.style.maxHeight = "150px"; 435 | this.preview.style.display = "none"; 436 | 437 | // Update preview when URL changes 438 | this.inputEl.addEventListener("input", () => { 439 | this.updatePreview(); 440 | }); 441 | 442 | // Buttons 443 | const buttonContainer = contentEl.createDiv(); 444 | buttonContainer.style.display = "flex"; 445 | buttonContainer.style.justifyContent = "flex-end"; 446 | buttonContainer.style.gap = "10px"; 447 | buttonContainer.style.marginTop = "1em"; 448 | 449 | const cancelButton = buttonContainer.createEl("button", { 450 | text: "Cancel", 451 | }); 452 | const saveButton = buttonContainer.createEl("button", { 453 | text: "Set Thumbnail", 454 | cls: "mod-cta", 455 | }); 456 | 457 | cancelButton.addEventListener("click", () => { 458 | this.resolvePromise(null); 459 | this.close(); 460 | }); 461 | 462 | saveButton.addEventListener("click", () => { 463 | this.resolvePromise(this.inputEl.value); 464 | this.close(); 465 | }); 466 | } 467 | 468 | openFilePicker() { 469 | const fileSuggestModal = new FileSuggestModal(this.app, (file) => { 470 | if (file) { 471 | this.inputEl.value = file.path; 472 | this.updatePreview(); 473 | } 474 | }); 475 | fileSuggestModal.open(); 476 | } 477 | 478 | updatePreview() { 479 | const url = this.inputEl.value; 480 | if (url) { 481 | try { 482 | // For remote URLs 483 | if (url.startsWith("http")) { 484 | this.preview.src = url; 485 | this.preview.style.display = "block"; 486 | } 487 | // For local files 488 | else { 489 | this.preview.src = 490 | this.app.vault.adapter.getResourcePath(url); 491 | this.preview.style.display = "block"; 492 | } 493 | } catch (e) { 494 | this.preview.style.display = "none"; 495 | } 496 | } else { 497 | this.preview.style.display = "none"; 498 | } 499 | } 500 | 501 | onClose() { 502 | const { contentEl } = this; 503 | contentEl.empty(); 504 | } 505 | 506 | openAndGetValue(): Promise { 507 | return new Promise((resolve) => { 508 | this.resolvePromise = resolve; 509 | this.open(); 510 | }); 511 | } 512 | } 513 | 514 | // File selection modal for picking a file from the vault 515 | class FileSuggestModal extends SuggestModal { 516 | private callback: (file: TFile | null) => void; 517 | 518 | constructor(app: App, callback: (file: TFile | null) => void) { 519 | super(app); 520 | this.callback = callback; 521 | } 522 | 523 | getSuggestions(query: string): TFile[] { 524 | const imageExtensions = [ 525 | "png", 526 | "jpg", 527 | "jpeg", 528 | "gif", 529 | "bmp", 530 | "svg", 531 | "webp", 532 | ]; 533 | 534 | // Get all image files from the vault 535 | const files = this.app.vault 536 | .getFiles() 537 | .filter( 538 | (file) => 539 | imageExtensions.includes(file.extension.toLowerCase()) && 540 | file.path.toLowerCase().includes(query.toLowerCase()) 541 | ); 542 | 543 | return files; 544 | } 545 | 546 | renderSuggestion(file: TFile, el: HTMLElement) { 547 | el.createEl("div", { text: file.path }); 548 | } 549 | 550 | onChooseSuggestion(file: TFile, evt: MouseEvent | KeyboardEvent) { 551 | this.callback(file); 552 | } 553 | 554 | onClose() { 555 | // If no selection was made, call the callback with null 556 | if (this.callback) { 557 | this.callback(null); 558 | } 559 | } 560 | } 561 | 562 | export const refreshAllCanvasView = (app: App) => { 563 | const cavasLeaves = app.workspace.getLeavesOfType("canvas"); 564 | if (!cavasLeaves || cavasLeaves.length === 0) return; 565 | for (const leaf of cavasLeaves) { 566 | leaf.rebuildView(); 567 | } 568 | }; 569 | 570 | export const getSelectionCoords = (dom: HTMLElement) => { 571 | const domHTML = dom.outerHTML; 572 | 573 | const translateRegex = /translate\((-?\d+\.?\d*)px, (-?\d+\.?\d*)px\)/; 574 | const sizeRegex = /width: (\d+\.?\d*)px; height: (\d+\.?\d*)px;/; 575 | const translateMatches = domHTML.match(translateRegex); 576 | const sizeMatches = domHTML.match(sizeRegex); 577 | if (translateMatches && sizeMatches) { 578 | const x = parseFloat(translateMatches[1]); 579 | const y = parseFloat(translateMatches[2]); 580 | 581 | const width = parseFloat(sizeMatches[1]); 582 | const height = parseFloat(sizeMatches[2]); 583 | 584 | return { 585 | minX: x, 586 | minY: y, 587 | maxX: x + width, 588 | maxY: y + height, 589 | }; 590 | } 591 | }; 592 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .canvas-node-collapse-control { 2 | z-index: 100; 3 | position: relative; 4 | 5 | overflow: hidden; 6 | 7 | min-width: 100%; 8 | height: calc(30px * var(--zoom-multiplier)); 9 | /*max-height: 100px;*/ 10 | min-height: max(calc(30px * var(--zoom-multiplier)), 20px); 11 | cursor: pointer; 12 | 13 | display: flex; 14 | align-items: center; 15 | padding-left: var(--size-4-2); 16 | gap: var(--size-4-3); 17 | margin-top: calc(30px * var(--zoom-multiplier) * -1); 18 | 19 | transition: background-color 0.2s ease-in-out; 20 | } 21 | 22 | .canvas-node-container:has(.canvas-node-content > img) 23 | .canvas-node-collapse-control { 24 | position: absolute; 25 | } 26 | 27 | .canvas-node-container:not(:has(.canvas-node-content > img)):has( 28 | .canvas-node-collapse-control 29 | ) { 30 | padding-top: calc(30px * var(--zoom-multiplier)); 31 | } 32 | 33 | .canvas-node-container:has(.canvas-node-content > img) 34 | .canvas-node-collapse-control { 35 | margin-top: 0; 36 | } 37 | 38 | .canvas-node:not(.collapsed) .canvas-node-collapse-control { 39 | background-color: rgba(var(--canvas-color), 0.15); 40 | } 41 | 42 | .canvas-node:not(.collapsed) .canvas-node-collapse-control:hover { 43 | background-color: rgba(var(--canvas-color), 0.5); 44 | } 45 | 46 | .canvas-node.collapsed .canvas-node-collapse-control { 47 | background-color: rgb(var(--canvas-color)); 48 | } 49 | 50 | .canvas-node:has(.canvas-node-collapse-control) { 51 | width: fit-content; 52 | height: fit-content; 53 | } 54 | 55 | /*.canvas-node-group:not(.is-focused):not(.is-selected) {*/ 56 | /* pointer-events: unset;*/ 57 | /*}*/ 58 | 59 | .canvas-node-group:has(.canvas-node-collapse-control) 60 | .canvas-node-collapse-control { 61 | pointer-events: initial !important; 62 | } 63 | 64 | /*.canvas-node.collapsed .canvas-node-collapse-control {*/ 65 | /* border-radius: var(--radius-m);*/ 66 | /*}*/ 67 | 68 | .canvas-node.collapsed { 69 | height: calc(30px * var(--zoom-multiplier)) !important; 70 | max-height: fit-content; 71 | transition: height 0.2s ease-in-out, max-height 0.2s ease-in-out; 72 | } 73 | 74 | .canvas-node.collapsed .canvas-node-placeholder { 75 | display: none; 76 | } 77 | 78 | .group-nodes-collapsed, 79 | .group-edges-collapsed { 80 | display: none; 81 | transition: opacity 0.2s ease-in-out; 82 | opacity: 0; 83 | } 84 | 85 | .canvas-node.collapsed .canvas-node-content { 86 | display: none; 87 | transition: opacity 0.2s ease-in-out; 88 | opacity: 0; 89 | } 90 | 91 | .collapsed-interaction { 92 | height: calc(30px * var(--zoom-multiplier)) !important; 93 | transition: height 0.2s ease-in-out; 94 | } 95 | 96 | .canvas-node-collapse-control-icon, 97 | .canvas-node-type-icon { 98 | display: flex; 99 | } 100 | 101 | .canvas-node-collapse-control-icon svg, 102 | .canvas-node-type-icon svg { 103 | height: calc(18px * var(--zoom-multiplier)); 104 | width: calc(18px * var(--zoom-multiplier)); 105 | } 106 | 107 | .canvas-node-collapse-control + .canvas-node-content.media-embed img { 108 | height: auto; 109 | max-height: 100%; 110 | object-fit: contain; 111 | } 112 | 113 | .minimal-control-header .canvas-node-collapse-control-title, 114 | .minimal-control-header .canvas-node-type-icon { 115 | display: none; 116 | } 117 | 118 | .canvas-node { 119 | /* Add transition for height and max-height only */ 120 | transition: height 0.2s ease-in-out, max-height 0.2s ease-in-out; 121 | } 122 | 123 | .canvas-node-content { 124 | /* Add transition for opacity */ 125 | transition: opacity 0.2s ease-in-out; 126 | opacity: 1; 127 | } 128 | 129 | .canvas-node.collapsed .canvas-node-content { 130 | opacity: 0; 131 | /* Keep the display:none but add it with a delay so the opacity transition is visible */ 132 | transition: opacity 0.2s ease-in-out; 133 | } 134 | 135 | /* Add rotation animation to the collapse icon */ 136 | .canvas-node-collapse-control-icon svg { 137 | transition: transform 0.2s ease; 138 | } 139 | 140 | .canvas-node.collapsed .canvas-node-collapse-control-icon svg { 141 | transform: rotate(-90deg); 142 | transition: transform 0.2s ease; 143 | } 144 | 145 | /* Remove hover scale effect that causes node drift */ 146 | /* .canvas-node:not(.collapsed):hover { 147 | transform: scale(1.01); 148 | } */ 149 | 150 | /* Animate the collapse control background */ 151 | .canvas-node-collapse-control { 152 | transition: background-color 0.2s ease-in-out; 153 | } 154 | 155 | /* Modify drawer animation to prevent drift */ 156 | .canvas-node-group .group-nodes, 157 | .canvas-node-group .group-edges { 158 | transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out; 159 | /* Remove transform transitions that cause drift */ 160 | } 161 | 162 | .canvas-node-group.collapsed .group-nodes-collapsed, 163 | .canvas-node-group.collapsed .group-edges-collapsed { 164 | opacity: 0; 165 | transition: opacity 0.2s ease-in-out; 166 | /* Remove transform that causes drift */ 167 | } 168 | 169 | /* Animation classes for transitions */ 170 | .node-transitioning { 171 | transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out !important; 172 | /* Remove transform transition that causes drift */ 173 | } 174 | 175 | .animating { 176 | overflow: hidden !important; 177 | } 178 | 179 | /* Simplified drawer animation to prevent drift */ 180 | .canvas-node-group .group-nodes { 181 | transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out; 182 | will-change: opacity, max-height; 183 | /* Remove transform properties that cause drift */ 184 | } 185 | 186 | .canvas-node-group.collapsed .group-nodes { 187 | max-height: 0; 188 | opacity: 0; 189 | transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out; 190 | /* Remove transform that causes drift */ 191 | } 192 | 193 | /* Animation for edges */ 194 | .canvas-edge { 195 | transition: opacity 0.2s ease-in-out; 196 | } 197 | 198 | .group-edges-collapsed { 199 | transition: opacity 0.2s ease-in-out; 200 | opacity: 0; 201 | } 202 | 203 | /* Remove bounce animation that causes drift */ 204 | /* @keyframes bounceIn { 205 | 0% { 206 | transform: scale(0.95); 207 | opacity: 0; 208 | } 209 | 70% { 210 | transform: scale(1.02); 211 | opacity: 1; 212 | } 213 | 100% { 214 | transform: scale(1); 215 | opacity: 1; 216 | } 217 | } 218 | 219 | .canvas-node:not(.collapsed):not(.animating) { 220 | animation: bounceIn 0.4s ease-out; 221 | } */ 222 | 223 | .collapse-handler.collapsed svg { 224 | transform: rotate(-90deg); 225 | transition: transform 0.2s ease-in-out; 226 | } 227 | 228 | .collapse-handler svg { 229 | transform: rotate(0deg); 230 | transition: transform 0.2s ease-in-out; 231 | } 232 | 233 | .canvas-node .canvas-node-collapse-control-thumbnail { 234 | display: none; 235 | } 236 | 237 | /* Styles for thumbnails and aliases in collapsed state */ 238 | .canvas-node.collapsed .canvas-node-collapse-control-thumbnail { 239 | max-width: 100%; 240 | max-height: 100px; 241 | object-fit: contain; 242 | margin: 5px auto; 243 | display: block; 244 | border-radius: 4px; 245 | position: absolute; 246 | bottom: calc(27px * var(--zoom-multiplier)); 247 | } 248 | 249 | .canvas-node.collapsed .canvas-node-collapse-control-alias { 250 | display: block; 251 | font-weight: bold; 252 | text-align: center; 253 | overflow: hidden; 254 | text-overflow: ellipsis; 255 | white-space: nowrap; 256 | 257 | color: var(--text-normal); 258 | } 259 | 260 | /* Adjust the header to accommodate thumbnails and aliases */ 261 | .canvas-node.collapsed .canvas-node-collapse-control { 262 | display: flex; 263 | flex-direction: row; 264 | width: 100%; 265 | } 266 | 267 | /* Make sure the header elements are properly spaced in collapsed mode */ 268 | .canvas-node .canvas-node-collapse-control-icon, 269 | .canvas-node .canvas-node-type-icon { 270 | margin-bottom: 4px; 271 | } 272 | 273 | body.hide-default-node-title 274 | .canvas-node:has(.canvas-node-collapse-control) 275 | .canvas-node-label { 276 | display: none; 277 | } 278 | 279 | .collapsed-node-hidden { 280 | display: none !important; 281 | } 282 | 283 | 284 | span.canvas-node-collapse-control-title { 285 | overflow: hidden; 286 | text-overflow: ellipsis; 287 | white-space: nowrap; 288 | max-width: 100%; 289 | } 290 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "1.1.0", 3 | "1.0.1": "1.1.0", 4 | "1.1.0": "1.1.0", 5 | "1.1.1": "1.1.0", 6 | "2.0.0": "1.1.0" 7 | } --------------------------------------------------------------------------------