├── .DS_Store ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── images ├── add-query.png ├── dataview-settings.png ├── edit-query.png ├── et-voila.png ├── select-position-to-insert.png ├── select-query.png ├── select-results.png └── set-append-prepend.png ├── main.ts ├── manifest.json ├── package.json ├── src ├── queries │ ├── Query.ts │ └── queryResultsModal.ts └── settings │ ├── QuerySetting.ts │ ├── QuerySettingModal.ts │ ├── settingTab.ts │ └── settings.ts ├── styles.css ├── tsconfig.json ├── types.d.ts └── versions.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | tab_width = 4 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "no-unused-vars": "off", 17 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "no-prototype-builtins": "off", 20 | "@typescript-eslint/no-empty-function": "off" 21 | } 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | package-lock.json 11 | 12 | # Don't include the compiled main.js file in the repo. 13 | # They should be uploaded to GitHub releases instead. 14 | main.js 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mathieu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | This plugin leverages the great Dataview plugin capabilities to select multiple values returned by a query and include them in your note 3 | 4 | You can beta-test it with BRAT: `mdelobelle/obsidian-multiselect/` 5 | 6 | for example 7 | - select some [[]] from your '#Ingredient' notes as required ingredients for a recipe 8 | - select some of your [[]] from your '#Staff' notes as participants for a meeting note 9 | - select some [[]] from your '#Favorite && #Song' notes as songs for a playlist note 10 | and so on... 11 | 12 | Seing all the links related to a query helps selecting them faster and not forgetting some. 13 | 14 | ### Settings 15 | 16 | > Important: Activate dataview js queries and inline js queries 17 | 18 | drawing 19 | 20 | 21 | You can Add/Change/Remove as many queries has you want 22 | 23 | Add a query by hitting "+". 24 | 25 | drawing 26 | 27 | You'll have to set a name, a description and the query. 28 | The query has to be written in the dataviewjs syntax: https://blacksmithgu.github.io/obsidian-dataview/api/intro/ and has to return a dataArray 29 | 30 | drawing 31 | 32 | There will be one command per query. Each one is name "Multi Select: {description of the query}" 33 | 34 | Once created, you can modify the query by hitting the pencil button or remove it by hitting the garbage button 35 | 36 | ### Usage 37 | 1. In live preview, position the cursor where you want to include links 38 | 39 | drawing 40 | 41 | 2. open the command palette and select the query that you want to execute 42 | 43 | drawing 44 | 45 | 3. Select the results that you want to paste at the cursor position 46 | 47 | drawing 48 | 49 | 4. You can select an alias for the link display when the target note contains aliases in its frontmatter 50 | 51 | 5. Set append and prepend strings (default prepend string: none, default append string: `", "`) 52 | 53 | drawing 54 | 55 | 6. Hit the checkmark button to paste the links in your note: Et voilà! 56 | 57 | drawing 58 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['main.ts'], 19 | bundle: true, 20 | external: ['obsidian', 'electron', 'obsidian-dataview', ...builtins], 21 | format: 'cjs', 22 | watch: !prod, 23 | target: 'es2016', 24 | logLevel: "info", 25 | sourcemap: prod ? false : 'inline', 26 | treeShaking: true, 27 | outfile: 'main.js', 28 | }).catch(() => process.exit(1)); 29 | -------------------------------------------------------------------------------- /images/add-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/add-query.png -------------------------------------------------------------------------------- /images/dataview-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/dataview-settings.png -------------------------------------------------------------------------------- /images/edit-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/edit-query.png -------------------------------------------------------------------------------- /images/et-voila.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/et-voila.png -------------------------------------------------------------------------------- /images/select-position-to-insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/select-position-to-insert.png -------------------------------------------------------------------------------- /images/select-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/select-query.png -------------------------------------------------------------------------------- /images/select-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/select-results.png -------------------------------------------------------------------------------- /images/set-append-prepend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdelobelle/obsidian-multiselect/7aac5a85162bf1f4d6b885a27123813196d82e37/images/set-append-prepend.png -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownView, Plugin } from 'obsidian'; 2 | import { MultiSelectSettings, DEFAULT_SETTINGS } from "src/settings/settings" 3 | import Query from "src/queries/Query" 4 | import QueryResultModal from "src/queries/queryResultsModal" 5 | import settingTab from "src/settings/settingTab" 6 | 7 | export default class MultiSelect extends Plugin { 8 | settings: MultiSelectSettings; 9 | initialQueries: Array = [] 10 | 11 | addMultiSelectQueryCommand(query: Query) { 12 | this.addCommand({ 13 | id: `multiSelect-${query.name}`, 14 | name: `Multi Select from ${query.name}`, 15 | callback: () => { 16 | const leaf = this.app.workspace.activeLeaf 17 | if (leaf.view instanceof MarkdownView && leaf.view.editor) { 18 | const queryResultModal = new QueryResultModal(this.app, this, query, leaf.view.editor.getCursor(), leaf.view.file) 19 | queryResultModal.open() 20 | } 21 | }, 22 | }); 23 | } 24 | 25 | async onload() { 26 | await this.loadSettings(); 27 | this.settings.queries.forEach(savedQuery => { 28 | const query = new Query() 29 | Object.assign(query, savedQuery) 30 | this.initialQueries.push(query) 31 | }) 32 | this.addSettingTab(new settingTab(this.app, this)); 33 | this.settings.queries.forEach(query => { 34 | this.addMultiSelectQueryCommand(query) 35 | }) 36 | 37 | } 38 | 39 | onunload() { 40 | 41 | } 42 | 43 | async loadSettings() { 44 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 45 | } 46 | 47 | async saveSettings() { 48 | this.settings.queries = this.initialQueries 49 | await this.saveData(this.settings); 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-multiselect", 3 | "name": "Multi Select", 4 | "version": "0.0.1", 5 | "minAppVersion": "0.13.19", 6 | "description": "Select multiple notes from a dataview query and include their links the current note", 7 | "author": "mdelobelle", 8 | "authorUrl": "https://github.com/mdelobelle", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-multiselect", 3 | "version": "0.0.1", 4 | "description": "Select multiple notes and include their links the current note", 5 | "main": "main.js", 6 | "types": "types.d.ts", 7 | "scripts": { 8 | "dev": "node esbuild.config.mjs", 9 | "build": "node esbuild.config.mjs production" 10 | }, 11 | "keywords": [], 12 | "author": "mdelobelle", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "^5.2.0", 17 | "@typescript-eslint/parser": "^5.2.0", 18 | "builtin-modules": "^3.2.0", 19 | "esbuild": "0.13.12", 20 | "obsidian": "^0.12.17", 21 | "obsidian-dataview": "^0.4.21", 22 | "tslib": "2.3.1", 23 | "typescript": "4.4.4" 24 | } 25 | } -------------------------------------------------------------------------------- /src/queries/Query.ts: -------------------------------------------------------------------------------- 1 | interface Query { 2 | id: string 3 | name: string 4 | description: string 5 | dataviewJSQuery?: string 6 | } 7 | 8 | class Query { 9 | 10 | constructor(name: string = "", 11 | description: string = "", 12 | id: string = "", 13 | dataviewJSQuery: string = null) { 14 | this.name = name 15 | this.description = description 16 | this.id = id 17 | this.dataviewJSQuery = dataviewJSQuery 18 | } 19 | 20 | static copyQuery(target: Query, source: Query) { 21 | target.id = source.id 22 | target.name = source.name 23 | target.description = source.description 24 | target.dataviewJSQuery = source.dataviewJSQuery 25 | } 26 | } 27 | 28 | export default Query -------------------------------------------------------------------------------- /src/queries/queryResultsModal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Modal, 4 | TFile, 5 | MarkdownView, 6 | EditorPosition, 7 | ButtonComponent, 8 | ExtraButtonComponent, 9 | setIcon, 10 | ToggleComponent, 11 | parseFrontMatterAliases, 12 | TextComponent, 13 | TextAreaComponent 14 | } from "obsidian" 15 | import MultiSelect from "main" 16 | import Query from "src/queries/Query" 17 | 18 | 19 | export default class QueryResultModal extends Modal { 20 | query: Query 21 | plugin: MultiSelect 22 | cursorPosition: EditorPosition 23 | results: Array 24 | selectedResults: string[] 25 | selectedAlias: Record 26 | file: TFile 27 | prepend: string 28 | append: string 29 | 30 | constructor(app: App, plugin: MultiSelect, query: Query, cursorPosition: EditorPosition, file: TFile) { 31 | super(app) 32 | this.plugin = plugin 33 | this.query = query 34 | this.cursorPosition = cursorPosition 35 | this.results = [] 36 | this.selectedResults = [] 37 | this.selectedAlias = {} 38 | this.file = file 39 | this.prepend = "" 40 | this.append = ", " 41 | } 42 | 43 | onOpen() { 44 | //@ts-ignore 45 | const getResults = (api: DataviewPlugin["api"]) => { 46 | return (new Function("dv", `return ${this.query.dataviewJSQuery}`))(api) 47 | }; 48 | const valueGrid = this.contentEl.createDiv({ 49 | cls: "modal-results-grid" 50 | }) 51 | if (this.app.plugins.enabledPlugins.has("dataview")) { 52 | const api = this.app.plugins.plugins.dataview?.api; 53 | if (api) { 54 | this.results = getResults(api); 55 | } 56 | else 57 | this.plugin.registerEvent( 58 | this.app.metadataCache.on("dataview:api-ready", (api) => 59 | this.results = getResults(api) 60 | ) 61 | ); 62 | } 63 | this.populateValuesGrid(valueGrid, this.results.map((p: any) => p.file.path)) 64 | } 65 | 66 | buildAliasesList(destFile: TFile): string[] { 67 | const frontmatter = this.app.metadataCache.getFileCache(destFile).frontmatter 68 | return parseFrontMatterAliases(frontmatter) 69 | } 70 | 71 | buildValueToggler(valueGrid: HTMLDivElement, destFile: TFile, aliases?: string[]) { 72 | const valueSelectorContainer = valueGrid.createDiv({ 73 | cls: "value-selector-container" 74 | }) 75 | const valueTogglerLine = valueSelectorContainer.createDiv({ 76 | cls: "value-toggler-line" 77 | }) 78 | const valueTogglerContainer = valueTogglerLine.createDiv({ 79 | cls: "value-selector-toggler" 80 | }) 81 | const valueToggler = new ToggleComponent(valueTogglerContainer) 82 | valueToggler.onChange(value => { 83 | if (value && !this.selectedResults.includes(destFile.path)) { 84 | this.selectedResults.push(destFile.path) 85 | } 86 | if (!value) { 87 | this.selectedResults.remove(destFile.path) 88 | delete this.selectedAlias[destFile.basename] 89 | } 90 | }) 91 | const valueLabel = valueTogglerLine.createDiv({ 92 | cls: "value-selector-label" 93 | }) 94 | valueLabel.setText(destFile.basename) 95 | valueLabel.onClickEvent(e => valueToggler.setValue(!valueToggler.getValue())) 96 | if (aliases) { 97 | const aliasesSelectorContainer = valueTogglerLine.createDiv({ 98 | cls: "value-selector-aliases" 99 | }) 100 | setIcon(aliasesSelectorContainer, "three-horizontal-bars") 101 | const aliasesListContainer = valueSelectorContainer.createDiv({ 102 | cls: "aliases-list-container" 103 | }) 104 | aliasesListContainer.style.display = "none" 105 | aliasesSelectorContainer.onClickEvent(e => { 106 | if (aliasesListContainer.style.display === "none") { 107 | this.buildAliasSelector(aliasesListContainer, valueLabel, aliases, destFile.basename) 108 | aliasesListContainer.style.display = "inline-block" 109 | } else { 110 | aliasesListContainer.innerHTML = '' 111 | aliasesListContainer.style.display = "none" 112 | } 113 | }) 114 | } 115 | } 116 | 117 | buildAliasSelector(aliasesListContainer: HTMLDivElement, valueLabel: HTMLDivElement, aliases: string[], basename: string) { 118 | aliases.forEach(alias => { 119 | if (!Object.keys(this.selectedAlias).includes(basename) || this.selectedAlias[basename] !== alias) { 120 | const aliasContainer = aliasesListContainer.createDiv() 121 | aliasContainer.innerHTML = `• ${alias}` 122 | aliasContainer.onClickEvent(e => { 123 | valueLabel.setText(alias) 124 | this.selectedAlias[basename] = alias 125 | aliasesListContainer.innerHTML = "" 126 | aliasesListContainer.style.display = "none" 127 | }) 128 | } 129 | }) 130 | if (Object.keys(this.selectedAlias).includes(basename) && this.selectedAlias[basename] !== null) { 131 | const aliasContainer = aliasesListContainer.createDiv() 132 | aliasContainer.innerHTML = `• ${basename}` 133 | aliasContainer.onClickEvent(e => { 134 | valueLabel.setText(basename) 135 | this.selectedAlias[basename] = null 136 | aliasesListContainer.innerHTML = "" 137 | aliasesListContainer.style.display = "none" 138 | }) 139 | } 140 | } 141 | 142 | buildMarkDownLink(path: string) { 143 | const destFile = this.app.metadataCache.getFirstLinkpathDest(path, this.file.path) 144 | const link = this.app.fileManager.generateMarkdownLink( 145 | destFile, 146 | this.file.path, 147 | null, 148 | this.selectedAlias[destFile.basename] 149 | ) 150 | return link 151 | } 152 | 153 | buildNewLine(): void { 154 | const leaf = this.app.workspace.activeLeaf 155 | 156 | if (leaf.view instanceof MarkdownView && leaf.view.editor) { 157 | const editor = leaf.view.editor 158 | const lineAtCursor = editor.getLine(this.cursorPosition.line) 159 | const startLine = lineAtCursor.substr(0, this.cursorPosition.ch) 160 | const content = this.selectedResults.map(r => this.buildMarkDownLink(r)).map(l => this.prepend + l).join(this.append) 161 | const endLine = lineAtCursor.substr(this.cursorPosition.ch, lineAtCursor.length - this.cursorPosition.ch) 162 | editor.setLine(this.cursorPosition.line, startLine + content + endLine) 163 | } 164 | } 165 | 166 | populateValuesGrid(valueGrid: HTMLDivElement, filePaths: string[]) { 167 | filePaths.forEach(filePath => { 168 | const destFile = this.app.metadataCache.getFirstLinkpathDest(filePath, this.file.path) 169 | this.buildValueToggler(valueGrid, destFile, this.buildAliasesList(destFile)) 170 | }) 171 | const divider = this.contentEl.createDiv() 172 | divider.innerHTML = "
" 173 | 174 | const helper = this.contentEl.createDiv({ 175 | cls: "separator-helper-label" 176 | }) 177 | helper.setText("prepend/append strings to the links") 178 | const footer = this.contentEl.createDiv({ 179 | cls: "value-grid-footer" 180 | }) 181 | const separatorContainer = footer.createDiv({ 182 | cls: 'separator-container' 183 | }) 184 | const prepend = new TextComponent(separatorContainer) 185 | prepend.inputEl.size = 10 186 | prepend.setValue(this.prepend) 187 | const linkLabel = separatorContainer.createDiv({ 188 | cls: "separator-link-label" 189 | }) 190 | linkLabel.setText(" [[Link]] ") 191 | prepend.onChange(value => this.prepend = value) 192 | const append = new TextAreaComponent(separatorContainer) 193 | append.inputEl.cols = 3 194 | append.inputEl.rows = 2 195 | append.setValue(this.append) 196 | append.onChange(value => this.append = value) 197 | const buttonsContainer = footer.createDiv({ 198 | cls: 'buttons-container' 199 | }) 200 | const saveButton = new ButtonComponent(buttonsContainer) 201 | saveButton.setIcon("checkmark") 202 | saveButton.onClick(() => { 203 | console.log(this.selectedResults, this.selectedAlias) 204 | this.buildNewLine() 205 | this.close() 206 | }) 207 | const cancelButton = new ExtraButtonComponent(buttonsContainer) 208 | cancelButton.setIcon("cross") 209 | cancelButton.onClick(() => this.close()) 210 | } 211 | } -------------------------------------------------------------------------------- /src/settings/QuerySetting.ts: -------------------------------------------------------------------------------- 1 | import { App, Setting } from "obsidian" 2 | import MultiSelect from "main" 3 | import Query from "src/queries/Query" 4 | import QuerySettingModal from "src/settings/QuerySettingModal" 5 | 6 | export default class QuerySetting extends Setting { 7 | query: Query 8 | app: App 9 | plugin: MultiSelect 10 | containerEl: HTMLElement 11 | constructor(containerEl: HTMLElement, query: Query, app: App, plugin: MultiSelect) { 12 | super(containerEl) 13 | this.containerEl = containerEl 14 | this.query = query 15 | this.app = app 16 | this.plugin = plugin 17 | this.setTextContentWithname() 18 | this.addEditButton() 19 | this.addDeleteButton() 20 | } 21 | 22 | setTextContentWithname(): void { 23 | this.setName(this.query.name) 24 | this.setDesc(this.query.description) 25 | } 26 | 27 | 28 | addEditButton(): void { 29 | this.addButton((b) => { 30 | b.setIcon("pencil") 31 | .setTooltip("Edit") 32 | .onClick(() => { 33 | let modal = new QuerySettingModal(this.app, this.plugin, this.containerEl, this, this.query); 34 | modal.open(); 35 | }); 36 | }) 37 | } 38 | 39 | addDeleteButton(): void { 40 | this.addButton((b) => { 41 | b.setIcon("trash") 42 | .setTooltip("Delete") 43 | .onClick(() => { 44 | //remove the command 45 | const currentExistingQuery = this.plugin.initialQueries.filter(p => p.id == this.query.id)[0] 46 | if (currentExistingQuery) { 47 | this.plugin.initialQueries.remove(currentExistingQuery) 48 | //@ts-ignore 49 | this.app.commands.removeCommand( 50 | `${this.plugin.manifest.id}:multiSelect-${currentExistingQuery.name}` 51 | ); 52 | } 53 | this.settingEl.parentElement.removeChild(this.settingEl) 54 | this.plugin.saveSettings() 55 | }); 56 | }); 57 | } 58 | } -------------------------------------------------------------------------------- /src/settings/QuerySettingModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting, TextComponent, Notice, ButtonComponent, ExtraButtonComponent, TextAreaComponent } from "obsidian" 2 | import MultiSelect from "main" 3 | import Query from "src/queries/Query" 4 | import QuerySetting from "src/settings/QuerySetting" 5 | 6 | export default class QuerySettingsModal extends Modal { 7 | namePromptComponent: TextComponent 8 | descriptionPromptComponent: TextComponent 9 | queryPromptComponent: TextAreaComponent 10 | saved: boolean = false 11 | query: Query 12 | plugin: MultiSelect 13 | initialQuery: Query 14 | parentSetting: QuerySetting 15 | new: boolean = true 16 | parentSettingContainer: HTMLElement 17 | 18 | 19 | constructor(app: App, plugin: MultiSelect, parentSettingContainer: HTMLElement, parentSetting?: QuerySetting, query?: Query) { 20 | super(app) 21 | this.plugin = plugin 22 | this.parentSetting = parentSetting 23 | this.initialQuery = new Query() 24 | this.parentSettingContainer = parentSettingContainer 25 | if (query) { 26 | this.new = false 27 | this.query = query 28 | this.initialQuery.name = query.name 29 | this.initialQuery.id = query.id 30 | this.initialQuery.description = query.description 31 | } else { 32 | let newId = 1 33 | this.plugin.initialQueries.forEach(query => { 34 | if (parseInt(query.id) && parseInt(query.id) >= newId) { 35 | newId = parseInt(query.id) + 1 36 | } 37 | }) 38 | this.query = new Query() 39 | this.query.id = newId.toString() 40 | this.initialQuery.id = newId.toString() 41 | } 42 | } 43 | 44 | onOpen(): void { 45 | if (this.query.name == "") { 46 | this.titleEl.setText(`Add a query`) 47 | } else { 48 | this.titleEl.setText(`Manage query ${this.query.name}`) 49 | } 50 | this.createForm() 51 | } 52 | 53 | onClose(): void { 54 | Object.assign(this.query, this.initialQuery) 55 | if (!this.new) { 56 | this.parentSetting.setTextContentWithname() 57 | } else if (this.saved) { 58 | new QuerySetting(this.parentSettingContainer, this.query, this.app, this.plugin) 59 | } 60 | } 61 | 62 | createNameInputContainer(parentNode: HTMLDivElement): TextComponent { 63 | const queryNameContainerLabel = parentNode.createDiv() 64 | queryNameContainerLabel.setText(`Query Name:`) 65 | const input = new TextComponent(parentNode) 66 | const name = this.query.name 67 | input.setValue(name) 68 | input.setPlaceholder("Name of the query") 69 | input.onChange(value => { 70 | this.query.name = value 71 | this.titleEl.setText(`Manage ${this.query.name}`) 72 | QuerySettingsModal.removeValidationError(input) 73 | }) 74 | return input 75 | } 76 | 77 | createDescriptionInputContainer(parentNode: HTMLDivElement): TextComponent { 78 | const queryDescriptionContainerLabel = parentNode.createDiv() 79 | queryDescriptionContainerLabel.setText(`Query Description:`) 80 | const input = new TextComponent(parentNode) 81 | const description = this.query.description 82 | input.setValue(description) 83 | input.setPlaceholder("Description of the query") 84 | input.onChange(value => { 85 | this.query.description = value 86 | QuerySettingsModal.removeValidationError(input) 87 | }) 88 | return input 89 | } 90 | 91 | createDataviewJSInputContainer(parentNode: HTMLDivElement): TextAreaComponent { 92 | const queryDataviewJSQueryContainerLabel = parentNode.createDiv() 93 | queryDataviewJSQueryContainerLabel.setText(`DataviewJS query:`) 94 | const input = new TextAreaComponent(parentNode) 95 | const dataviewJSQuery = this.query.dataviewJSQuery 96 | input.inputEl.cols = 100 97 | input.inputEl.rows = 15 98 | input.setPlaceholder("Dataviewjs syntax to query pages\nExample:\ndv.pages(\"#SomeTag\").where(p => p.field === \"some value\").sort(p => condition, 'asc')") 99 | input.setValue(dataviewJSQuery ?? "") 100 | input.onChange(value => { 101 | this.query.dataviewJSQuery = value 102 | QuerySettingsModal.removeValidationError(input) 103 | }) 104 | return input 105 | } 106 | 107 | createForm(): void { 108 | const div = this.contentEl.createDiv({ 109 | cls: "frontmatter-prompt-div" 110 | }) 111 | const mainDiv = div.createDiv({ 112 | cls: "frontmatter-prompt-form" 113 | }) 114 | /* Property Name Section */ 115 | const nameContainer = mainDiv.createDiv() 116 | const descriptionContainer = mainDiv.createDiv() 117 | const dataviewJSQueryContainer = mainDiv.createDiv() 118 | this.namePromptComponent = this.createNameInputContainer(nameContainer) 119 | this.descriptionPromptComponent = this.createDescriptionInputContainer(descriptionContainer) 120 | this.queryPromptComponent = this.createDataviewJSInputContainer(dataviewJSQueryContainer) 121 | 122 | mainDiv.createDiv().createEl("hr") 123 | 124 | /* footer buttons*/ 125 | const footerEl = this.contentEl.createDiv() 126 | const footerButtons = new Setting(footerEl) 127 | footerButtons.addButton((b) => this.createSaveButton(b)) 128 | footerButtons.addExtraButton((b) => this.createCancelButton(b)); 129 | } 130 | 131 | createSaveButton(b: ButtonComponent): ButtonComponent { 132 | b.setTooltip("Save") 133 | .setIcon("checkmark") 134 | .onClick(async () => { 135 | let error = false 136 | if (/^[#>-]/.test(this.query.name)) { 137 | QuerySettingsModal.setValidationError( 138 | this.namePromptComponent, this.namePromptComponent.inputEl, 139 | "Query name cannot start with #, >, -" 140 | ); 141 | error = true; 142 | } 143 | if (this.query.name == "") { 144 | QuerySettingsModal.setValidationError( 145 | this.namePromptComponent, this.namePromptComponent.inputEl, 146 | "Property name can not be Empty" 147 | ); 148 | error = true 149 | } 150 | if (error) { 151 | new Notice("Fix errors before saving."); 152 | return; 153 | } 154 | this.saved = true; 155 | const currentExistingQuery = this.plugin.initialQueries.filter(q => q.id == this.query.id)[0] 156 | if (currentExistingQuery) { 157 | this.plugin.initialQueries.remove(currentExistingQuery) 158 | //@ts-ignore 159 | this.app.commands.removeCommand( 160 | `${this.plugin.manifest.id}:multiSelect-${currentExistingQuery.name}` 161 | ); 162 | } 163 | this.plugin.initialQueries.push(this.query) 164 | this.plugin.addMultiSelectQueryCommand(this.query) 165 | this.initialQuery = this.query 166 | this.plugin.saveSettings() 167 | this.close(); 168 | }) 169 | return b 170 | } 171 | 172 | createCancelButton(b: ExtraButtonComponent): ExtraButtonComponent { 173 | b.setIcon("cross") 174 | .setTooltip("Cancel") 175 | .onClick(() => { 176 | this.saved = false; 177 | /* reset values from settings */ 178 | if (this.initialQuery.name != "") { 179 | Object.assign(this.query, this.initialQuery) 180 | } 181 | this.close(); 182 | }); 183 | return b; 184 | } 185 | 186 | /* utils functions */ 187 | 188 | static setValidationError(textInput: TextComponent | TextAreaComponent, insertAfter: Element, message?: string) { 189 | textInput.inputEl.addClass("is-invalid"); 190 | if (message) { 191 | 192 | let mDiv = textInput.inputEl.parentElement.querySelector( 193 | ".invalid-feedback" 194 | ) as HTMLDivElement; 195 | 196 | if (!mDiv) { 197 | mDiv = createDiv({ cls: "invalid-feedback" }); 198 | } 199 | mDiv.innerText = message; 200 | mDiv.insertAfter(insertAfter); 201 | } 202 | } 203 | static removeValidationError(textInput: TextComponent | TextAreaComponent) { 204 | if (textInput.inputEl.hasClass("is-invalid")) { 205 | textInput.inputEl.removeClass("is-invalid") 206 | textInput.inputEl.parentElement.removeChild( 207 | textInput.inputEl.parentElement.lastElementChild 208 | ) 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /src/settings/settingTab.ts: -------------------------------------------------------------------------------- 1 | import { PluginSettingTab, App, Setting, ButtonComponent } from "obsidian" 2 | import MultiSelect from "main" 3 | import Query from "src/queries/Query" 4 | import QuerySetting from 'src/settings/QuerySetting' 5 | import QuerySettingModal from 'src/settings/QuerySettingModal' 6 | 7 | export default class MultiSelectSettingTab extends PluginSettingTab { 8 | plugin: MultiSelect 9 | 10 | constructor(app: App, plugin: MultiSelect) { 11 | super(app, plugin); 12 | this.plugin = plugin; 13 | } 14 | 15 | display(): void { 16 | const { containerEl } = this; 17 | 18 | containerEl.empty(); 19 | 20 | containerEl.createEl('h2', { text: 'Settings for Multi Select.' }); 21 | 22 | /* Add new query*/ 23 | new Setting(containerEl) 24 | .setName("Add New Query") 25 | .setDesc("Add a new query to select files from result.") 26 | .addButton((button: ButtonComponent): ButtonComponent => { 27 | let b = button 28 | .setTooltip("Add New Query") 29 | .setButtonText("+") 30 | .onClick(async () => { 31 | let modal = new QuerySettingModal(this.app, this.plugin, containerEl); 32 | modal.open(); 33 | }); 34 | 35 | return b; 36 | }); 37 | 38 | /* Managed properties that currently have preset values */ 39 | this.plugin.initialQueries.forEach(savedQuery => { 40 | const query = new Query() 41 | Object.assign(query, savedQuery) 42 | new QuerySetting(containerEl, query, this.app, this.plugin) 43 | }) 44 | } 45 | } -------------------------------------------------------------------------------- /src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import Query from "src/queries/Query" 2 | 3 | export interface MultiSelectSettings { 4 | queries: Array 5 | } 6 | 7 | export const DEFAULT_SETTINGS: MultiSelectSettings = { 8 | queries: [] 9 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Sets all the text color to red! */ 2 | .modal-values-grid { 3 | display: grid; 4 | grid-template-columns: repeat(2, 1fr); 5 | } 6 | 7 | .value-selector-container{ 8 | margin-bottom: 5px; 9 | display: inline 10 | } 11 | 12 | .value-selector-toggler { 13 | display: inline-block; 14 | vertical-align: top; 15 | margin-right: 10px; 16 | padding-top: 3px; 17 | } 18 | 19 | .value-selector-label{ 20 | display: inline-block; 21 | margin-left: 10px; 22 | } 23 | 24 | .value-selector-aliases{ 25 | display: inline-block; 26 | margin-left: 10px; 27 | } 28 | 29 | .aliases-list-container{ 30 | margin-left: 62px; 31 | } 32 | 33 | .value-grid-footer { 34 | align-items: center; 35 | display: flex; 36 | justify-content: space-between; 37 | } 38 | 39 | .separator-container { 40 | display: flex; 41 | justify-content: flex-end; 42 | align-items: center; 43 | } 44 | 45 | .separator-label { 46 | margin-right: 10px; 47 | } 48 | 49 | .separator-link-label { 50 | margin-left: 10px; 51 | margin-right: 10px; 52 | color: var(--text-muted); 53 | font-size: 14px; 54 | } 55 | 56 | .separator-helper-label{ 57 | color: var(--text-muted); 58 | font-size: 14px; 59 | } 60 | 61 | .buttons-container { 62 | display: flex; 63 | align-items: center; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /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 | "lib": [ 13 | "DOM", 14 | "ES5", 15 | "ES6", 16 | "ES7" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | import { DataviewApi } from "obsidian-dataview"; 3 | 4 | declare module "obsidian" { 5 | interface App { 6 | plugins: { 7 | enabledPlugins: Set; 8 | plugins: { 9 | [id: string]: any; 10 | dataview?: { 11 | api?: DataviewApi; 12 | }; 13 | }; 14 | }; 15 | } 16 | interface MetadataCache { 17 | on( 18 | name: "dataview:api-ready", 19 | callback: (api: DataviewPlugin["api"]) => any, 20 | ctx?: any 21 | ): EventRef; 22 | on( 23 | name: "dataview:metadata-change", 24 | callback: ( 25 | ...args: 26 | | [op: "rename", file: TAbstractFile, oldPath: string] 27 | | [op: "delete", file: TFile] 28 | | [op: "update", file: TFile] 29 | ) => any, 30 | ctx?: any 31 | ): EventRef; 32 | } 33 | } -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.13.19" 3 | } --------------------------------------------------------------------------------