├── .gitignore ├── README.md ├── main.ts ├── manifest.json ├── package.json ├── rollup.config.js ├── styles.css ├── tsconfig.json └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Obsidian force note view mode 2 | 3 | This plug-in allows you to indicate through front matter that a note should always be opened in a certain view mode or editing mode. 4 | 5 | Changing the **view mode** can be done through the key `obsidianUIMode`, which can have the value `source` or `preview`. Changing the **editing mode** happens by declaring the key `obsidianEditingMode`; it takes `live` or `source` as value. 6 | 7 | Example: add below snippet (front matter) to your note ... 8 | ``` 9 | --- 10 | obsidianUIMode: source 11 | obsidianEditingMode: live 12 | --- 13 | ``` 14 | ... and this will force the note to open in "live preview" edit mode. 15 | 16 | 17 | Similar, ... add below snippet to your note ... 18 | ``` 19 | --- 20 | obsidianUIMode: preview 21 | --- 22 | ``` 23 | ... and this will always open the note in a reading (/ preview) mode. 24 | 25 | This plug-in also ensures that a note is always opened in the configured default mode (suppose the Obsidian setting has "preview" as default mode but the pane is currently in "source" mode, then opening a new note in that same pane will open in "preview" mode). 26 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkspaceLeaf, 3 | Plugin, 4 | MarkdownView, 5 | App, 6 | TFile, 7 | TFolder, 8 | PluginSettingTab, 9 | Setting, 10 | debounce, 11 | } from "obsidian"; 12 | 13 | interface ViewModeByFrontmatterSettings { 14 | debounceTimeout: number; 15 | ignoreOpenFiles: boolean; 16 | ignoreForceViewAll: boolean; 17 | folders: {folder: string, viewMode: string}[]; 18 | files: {filePattern: string; viewMode: string}[]; 19 | } 20 | 21 | const DEFAULT_SETTINGS: ViewModeByFrontmatterSettings = { 22 | debounceTimeout: 300, 23 | ignoreOpenFiles: false, 24 | ignoreForceViewAll: false, 25 | folders: [{folder: '', viewMode: ''}], 26 | files: [{filePattern: '', viewMode: ''}], 27 | }; 28 | 29 | export default class ViewModeByFrontmatterPlugin extends Plugin { 30 | settings: ViewModeByFrontmatterSettings; 31 | 32 | OBSIDIAN_UI_MODE_KEY = "obsidianUIMode"; 33 | OBSIDIAN_EDITING_MODE_KEY = "obsidianEditingMode"; 34 | 35 | openedFiles: String[]; 36 | 37 | async onload() { 38 | await this.loadSettings(); 39 | 40 | this.addSettingTab(new ViewModeByFrontmatterSettingTab(this.app, this)); 41 | 42 | this.openedFiles = resetOpenedNotes(this.app); 43 | 44 | const readViewModeFromFrontmatterAndToggle = async ( 45 | leaf: WorkspaceLeaf 46 | ) => { 47 | let view = leaf.view instanceof MarkdownView ? leaf.view : null; 48 | 49 | if (null === view) { 50 | if (true == this.settings.ignoreOpenFiles) { 51 | this.openedFiles = resetOpenedNotes(this.app); 52 | } 53 | 54 | return; 55 | } 56 | 57 | // if setting is true, nothing to do if this was an open note 58 | if ( 59 | true == this.settings.ignoreOpenFiles && 60 | alreadyOpen(view.file, this.openedFiles) 61 | ) { 62 | this.openedFiles = resetOpenedNotes(this.app); 63 | 64 | return; 65 | } 66 | 67 | let state = leaf.getViewState(); 68 | 69 | // check if in a declared folder or file 70 | let folderOrFileModeState: {source: boolean, mode: string} | null = null; 71 | 72 | const setFolderOrFileModeState = (viewMode: string): void => { 73 | const [key, mode] = viewMode.split(":").map((s) => s.trim()); 74 | 75 | if (key === "default") { 76 | folderOrFileModeState = null; // ensures that no state is set 77 | return; 78 | } else if (!["live", "preview", "source"].includes(mode)) { 79 | return; 80 | } 81 | 82 | folderOrFileModeState = { ...state.state }; 83 | 84 | folderOrFileModeState.mode = mode; 85 | 86 | switch (key) { 87 | case this.OBSIDIAN_EDITING_MODE_KEY: { 88 | if (mode == "live") { 89 | folderOrFileModeState.source = false; 90 | folderOrFileModeState.mode = "source"; 91 | } else { 92 | folderOrFileModeState.source = true; 93 | } 94 | break; 95 | } 96 | case this.OBSIDIAN_UI_MODE_KEY: 97 | folderOrFileModeState.source = false; 98 | break; 99 | } 100 | }; 101 | 102 | for (const folderMode of this.settings.folders) { 103 | if (folderMode.folder !== '' && folderMode.viewMode) { 104 | const folder = this.app.vault.getAbstractFileByPath(folderMode.folder); 105 | if (folder instanceof TFolder) { 106 | if (view.file.parent === folder || view.file.parent.path.startsWith(folder.path)) { 107 | if (!state.state) { // just to be on the safe side 108 | continue 109 | } 110 | 111 | setFolderOrFileModeState(folderMode.viewMode); 112 | } 113 | } else { 114 | console.warn(`ForceViewMode: Folder ${folderMode.folder} does not exist or is not a folder.`); 115 | } 116 | } 117 | } 118 | 119 | for (const { filePattern, viewMode } of this.settings.files) { 120 | if (!filePattern || !viewMode) { 121 | continue; 122 | } 123 | 124 | if (!state.state) { 125 | // just to be on the safe side 126 | continue; 127 | } 128 | 129 | if (!view.file.basename.match(filePattern)) { 130 | continue; 131 | } 132 | 133 | setFolderOrFileModeState(viewMode); 134 | } 135 | 136 | if (folderOrFileModeState) { 137 | if (state.state.mode !== folderOrFileModeState.mode || 138 | state.state.source !== folderOrFileModeState.source) { 139 | state.state.mode = folderOrFileModeState.mode; 140 | state.state.source = folderOrFileModeState.source; 141 | 142 | await leaf.setViewState(state); 143 | } 144 | 145 | return; 146 | } 147 | 148 | // ... get frontmatter data and search for a key indicating the desired view mode 149 | // and when the given key is present ... set it to the declared mode 150 | const fileCache = this.app.metadataCache.getFileCache(view.file); 151 | const fileDeclaredUIMode = 152 | fileCache !== null && fileCache.frontmatter 153 | ? fileCache.frontmatter[this.OBSIDIAN_UI_MODE_KEY] 154 | : null; 155 | const fileDeclaredEditingMode = 156 | fileCache !== null && fileCache.frontmatter 157 | ? fileCache.frontmatter[this.OBSIDIAN_EDITING_MODE_KEY] 158 | : null; 159 | 160 | 161 | if (fileDeclaredUIMode) { 162 | if ( 163 | ["source", "preview", "live"].includes(fileDeclaredUIMode) && 164 | view.getMode() !== fileDeclaredUIMode 165 | ) { 166 | state.state.mode = fileDeclaredUIMode; 167 | } 168 | } 169 | 170 | if (fileDeclaredEditingMode) { 171 | const shouldBeSourceMode = fileDeclaredEditingMode == 'source'; 172 | if ( 173 | ["source", "live"].includes(fileDeclaredEditingMode) 174 | ) { 175 | state.state.source = shouldBeSourceMode; 176 | } 177 | } 178 | 179 | if (fileDeclaredUIMode || fileDeclaredEditingMode) { 180 | await leaf.setViewState(state); 181 | 182 | if (true == this.settings.ignoreOpenFiles) { 183 | this.openedFiles = resetOpenedNotes(this.app); 184 | } 185 | 186 | return; 187 | } 188 | 189 | const defaultViewMode = this.app.vault.config.defaultViewMode 190 | ? this.app.vault.config.defaultViewMode 191 | : "source"; 192 | 193 | const defaultEditingModeIsLivePreview = this.app.vault.config.livePreview === undefined ? true : this.app.vault.config.livePreview; 194 | 195 | if (!this.settings.ignoreForceViewAll) { 196 | let state = leaf.getViewState(); 197 | 198 | if (view.getMode() !== defaultViewMode) { 199 | state.state.mode = defaultViewMode; 200 | } 201 | 202 | state.state.source = defaultEditingModeIsLivePreview ? false : true; 203 | 204 | await leaf.setViewState(state); 205 | 206 | this.openedFiles = resetOpenedNotes(this.app); 207 | } 208 | 209 | return; 210 | }; 211 | 212 | // "active-leaf-change": open note, navigate to note -> will check whether 213 | // the view mode needs to be set; default view mode setting is ignored. 214 | this.registerEvent( 215 | this.app.workspace.on( 216 | "active-leaf-change", 217 | this.settings.debounceTimeout === 0 218 | ? readViewModeFromFrontmatterAndToggle 219 | : debounce( 220 | readViewModeFromFrontmatterAndToggle, 221 | this.settings.debounceTimeout 222 | ) 223 | ) 224 | ); 225 | } 226 | 227 | async loadSettings() { 228 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 229 | } 230 | 231 | async saveSettings() { 232 | await this.saveData(this.settings); 233 | } 234 | 235 | async onunload() { 236 | this.openedFiles = []; 237 | } 238 | } 239 | 240 | function alreadyOpen(currFile: TFile, openedFiles: String[]): boolean { 241 | const leavesWithSameNote: String[] = []; 242 | 243 | if (currFile == null) { 244 | return false; 245 | } 246 | 247 | openedFiles.forEach((openedFile: String) => { 248 | if (openedFile == currFile.basename) { 249 | leavesWithSameNote.push(openedFile); 250 | } 251 | }); 252 | 253 | return leavesWithSameNote.length != 0; 254 | } 255 | 256 | function resetOpenedNotes(app: App): String[] { 257 | let openedFiles: String[] = []; 258 | 259 | app.workspace.iterateAllLeaves((leaf) => { 260 | let view = leaf.view instanceof MarkdownView ? leaf.view : null; 261 | 262 | if (null === view) { 263 | return; 264 | } 265 | 266 | openedFiles.push(leaf.view?.file?.basename); 267 | }); 268 | 269 | return openedFiles; 270 | } 271 | 272 | class ViewModeByFrontmatterSettingTab extends PluginSettingTab { 273 | plugin: ViewModeByFrontmatterPlugin; 274 | 275 | constructor(app: App, plugin: ViewModeByFrontmatterPlugin) { 276 | super(app, plugin); 277 | this.plugin = plugin; 278 | } 279 | 280 | display(): void { 281 | let { containerEl } = this; 282 | 283 | containerEl.empty(); 284 | 285 | const createHeader = (text: string) => containerEl.createEl("h2", { text }); 286 | 287 | const desc = document.createDocumentFragment(); 288 | desc.append( 289 | "Changing the view mode can be done through the key ", 290 | desc.createEl("code", { text: "obsidianUIMode" }), 291 | ", which can have the value ", 292 | desc.createEl("code", { text: "source" }), 293 | " or ", 294 | desc.createEl("code", { text: "preview" }), 295 | ".", 296 | desc.createEl("br"), 297 | "Changing the editing mode happens by declaring the key ", 298 | desc.createEl("code", { text: "obsidianEditingMode" }), 299 | "; it takes ", 300 | desc.createEl("code", { text: "live" }), 301 | " or ", 302 | desc.createEl("code", { text: "source" }), 303 | " as value." 304 | ); 305 | 306 | new Setting(this.containerEl).setDesc(desc); 307 | 308 | new Setting(containerEl) 309 | .setName("Ignore opened files") 310 | .setDesc("Never change the view mode on a note which was already open.") 311 | .addToggle((checkbox) => 312 | checkbox 313 | .setValue(this.plugin.settings.ignoreOpenFiles) 314 | .onChange(async (value) => { 315 | this.plugin.settings.ignoreOpenFiles = value; 316 | await this.plugin.saveSettings(); 317 | }) 318 | ); 319 | new Setting(containerEl) 320 | .setName("Ignore force view when not in frontmatter") 321 | .setDesc( 322 | "Never change the view mode on a note that was opened from another one in a certain view mode" 323 | ) 324 | .addToggle((checkbox) => { 325 | checkbox 326 | .setValue(this.plugin.settings.ignoreForceViewAll) 327 | .onChange(async (value) => { 328 | this.plugin.settings.ignoreForceViewAll = value; 329 | await this.plugin.saveSettings(); 330 | }); 331 | }); 332 | 333 | new Setting(containerEl) 334 | .setName("Debounce timeout in milliseconds") 335 | .setDesc(`Debounce timeout is the time in milliseconds after which the view mode is set. Set "0" to disable debouncing (default value is "300"). If you experience issues with the plugin, try increasing this value.`) 336 | .addText((cb) => { 337 | cb.setValue(String(this.plugin.settings.debounceTimeout)).onChange(async (value) => { 338 | this.plugin.settings.debounceTimeout = Number(value); 339 | 340 | await this.plugin.saveSettings(); 341 | }); 342 | }); 343 | 344 | const modes = [ 345 | "default", 346 | "obsidianUIMode: preview", 347 | "obsidianUIMode: source", 348 | "obsidianEditingMode: live", 349 | "obsidianEditingMode: source", 350 | ] 351 | 352 | createHeader("Folders") 353 | 354 | const folderDesc = document.createDocumentFragment(); 355 | folderDesc.append( 356 | "Specify a view mode for notes in a given folder.", 357 | folderDesc.createEl("br"), 358 | "Note that this will force the view mode on all the notes in the folder, even if they have a different view mode set in their frontmatter.", 359 | folderDesc.createEl("br"), 360 | "Precedence is from bottom (highest) to top (lowest), so if you have child folders specified, make sure to put them below their parent folder." 361 | ); 362 | 363 | new Setting(this.containerEl).setDesc(folderDesc); 364 | 365 | new Setting(this.containerEl) 366 | .setDesc("Add new folder") 367 | .addButton((button) => { 368 | button 369 | .setTooltip("Add another folder to the list") 370 | .setButtonText("+") 371 | .setCta() 372 | .onClick(async () => { 373 | this.plugin.settings.folders.push({ 374 | folder: "", 375 | viewMode: "", 376 | }); 377 | await this.plugin.saveSettings(); 378 | this.display(); 379 | }); 380 | }); 381 | 382 | 383 | this.plugin.settings.folders.forEach( 384 | (folderMode, index) => { 385 | const div = containerEl.createEl("div"); 386 | div.addClass("force-view-mode-div") 387 | div.addClass("force-view-mode-folder") 388 | 389 | const s = new Setting(this.containerEl) 390 | .addSearch((cb) => { 391 | cb.setPlaceholder("Example: folder1/templates") 392 | .setValue(folderMode.folder) 393 | .onChange(async (newFolder) => { 394 | if ( 395 | newFolder && 396 | this.plugin.settings.folders.some( 397 | (e) => e.folder == newFolder 398 | ) 399 | ) { 400 | console.error("ForceViewMode: This folder already has a template associated with", newFolder); 401 | 402 | return; 403 | } 404 | 405 | this.plugin.settings.folders[ 406 | index 407 | ].folder = newFolder; 408 | 409 | await this.plugin.saveSettings(); 410 | }); 411 | }) 412 | .addDropdown(cb => { 413 | modes.forEach(mode => { 414 | cb.addOption(mode, mode); 415 | }); 416 | 417 | cb.setValue(folderMode.viewMode || "default") 418 | .onChange(async (value) => { 419 | this.plugin.settings.folders[ 420 | index 421 | ].viewMode = value; 422 | 423 | await this.plugin.saveSettings(); 424 | }); 425 | }) 426 | .addExtraButton((cb) => { 427 | cb.setIcon("cross") 428 | .setTooltip("Delete") 429 | .onClick(async () => { 430 | this.plugin.settings.folders.splice( 431 | index, 432 | 1 433 | ); 434 | 435 | await this.plugin.saveSettings(); 436 | 437 | this.display(); 438 | }); 439 | }); 440 | 441 | s.infoEl.remove(); 442 | 443 | div.appendChild(containerEl.lastChild as Node); 444 | } 445 | ); 446 | 447 | createHeader("Files"); 448 | 449 | const filesDesc = document.createDocumentFragment(); 450 | filesDesc.append( 451 | "Specify a view mode for notes with specific patterns (regular expression; example \" - All$\" for all notes ending with \" - All\" or \"1900-01\" for all daily notes starting with \"1900-01\"", 452 | filesDesc.createEl("br"), 453 | "Note that this will force the view mode, even if it have a different view mode set in its frontmatter.", 454 | filesDesc.createEl("br"), 455 | "Precedence is from bottom (highest) to top (lowest).", 456 | filesDesc.createEl("br"), 457 | "Notice that configuring a file pattern will override the folder configuration for the same file." 458 | ); 459 | 460 | new Setting(this.containerEl).setDesc(filesDesc); 461 | 462 | new Setting(this.containerEl) 463 | .setDesc("Add new file") 464 | .addButton((button) => { 465 | button 466 | .setTooltip("Add another file to the list") 467 | .setButtonText("+") 468 | .setCta() 469 | .onClick(async () => { 470 | this.plugin.settings.files.push({ 471 | filePattern: "", 472 | viewMode: "", 473 | }); 474 | await this.plugin.saveSettings(); 475 | this.display(); 476 | }); 477 | }); 478 | 479 | this.plugin.settings.files.forEach((file, index) => { 480 | const div = containerEl.createEl("div"); 481 | div.addClass("force-view-mode-div"); 482 | div.addClass("force-view-mode-folder"); 483 | 484 | const s = new Setting(this.containerEl) 485 | .addSearch((cb) => { 486 | cb.setPlaceholder(`Example: " - All$" or "1900-01")`) 487 | .setValue(file.filePattern) 488 | .onChange(async (value) => { 489 | if ( 490 | value && 491 | this.plugin.settings.files.some((e) => e.filePattern == value) 492 | ) { 493 | console.error("ForceViewMode: Pattern already exists", value); 494 | 495 | return; 496 | } 497 | 498 | this.plugin.settings.files[index].filePattern = value; 499 | 500 | await this.plugin.saveSettings(); 501 | }); 502 | }) 503 | .addDropdown((cb) => { 504 | modes.forEach((mode) => { 505 | cb.addOption(mode, mode); 506 | }); 507 | 508 | cb.setValue(file.viewMode || "default").onChange(async (value) => { 509 | this.plugin.settings.files[index].viewMode = value; 510 | 511 | await this.plugin.saveSettings(); 512 | }); 513 | }) 514 | .addExtraButton((cb) => { 515 | cb.setIcon("cross") 516 | .setTooltip("Delete") 517 | .onClick(async () => { 518 | this.plugin.settings.files.splice(index, 1); 519 | 520 | await this.plugin.saveSettings(); 521 | 522 | this.display(); 523 | }); 524 | }); 525 | 526 | s.infoEl.remove(); 527 | 528 | div.appendChild(containerEl.lastChild as Node); 529 | }); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-view-mode-by-frontmatter", 3 | "name": "Force note view mode", 4 | "version": "1.2.2", 5 | "minAppVersion": "0.9.12", 6 | "description": "This plugin allows to force the view mode and editing mode for a note by using front matter", 7 | "author": "Benny Wydooghe", 8 | "authorUrl": "https://i-net.be", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "0.12.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^18.0.0", 15 | "@rollup/plugin-node-resolve": "^11.2.1", 16 | "@rollup/plugin-typescript": "^8.3.2", 17 | "@types/node": "^14.18.18", 18 | "obsidian": "latest", 19 | "rollup": "^2.75.1", 20 | "tslib": "^2.4.0", 21 | "typescript": "^4.7.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .force-view-mode-div > .setting-item > .setting-item-control { 2 | justify-content: space-around; 3 | padding: 0; 4 | width: 100%; 5 | } 6 | 7 | .force-view-mode-folder > .setting-item > .setting-item-control > *:first-child { 8 | flex: 2 1 0; 9 | } 10 | 11 | .force-view-mode-folder > .setting-item > .setting-item-control > *:nth-child(2) { 12 | flex: 1 1 0; 13 | } -------------------------------------------------------------------------------- /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 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.9.12" 3 | } 4 | --------------------------------------------------------------------------------