digital-garden/.obsidian/plugins/obsidian-view-mode-by-frontmatter/main.js

397 lines
40 KiB
JavaScript
Raw Permalink Normal View History

2024-06-13 21:01:37 +03:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
'use strict';
var obsidian = require('obsidian');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
const DEFAULT_SETTINGS = {
debounceTimeout: 300,
ignoreOpenFiles: false,
ignoreForceViewAll: false,
folders: [{ folder: '', viewMode: '' }],
files: [{ filePattern: '', viewMode: '' }],
};
class ViewModeByFrontmatterPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
this.OBSIDIAN_UI_MODE_KEY = "obsidianUIMode";
this.OBSIDIAN_EDITING_MODE_KEY = "obsidianEditingMode";
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadSettings();
this.addSettingTab(new ViewModeByFrontmatterSettingTab(this.app, this));
this.openedFiles = resetOpenedNotes(this.app);
const readViewModeFromFrontmatterAndToggle = (leaf) => __awaiter(this, void 0, void 0, function* () {
let view = leaf.view instanceof obsidian.MarkdownView ? leaf.view : null;
if (null === view) {
if (true == this.settings.ignoreOpenFiles) {
this.openedFiles = resetOpenedNotes(this.app);
}
return;
}
// if setting is true, nothing to do if this was an open note
if (true == this.settings.ignoreOpenFiles &&
alreadyOpen(view.file, this.openedFiles)) {
this.openedFiles = resetOpenedNotes(this.app);
return;
}
let state = leaf.getViewState();
// check if in a declared folder or file
let folderOrFileModeState = null;
const setFolderOrFileModeState = (viewMode) => {
const [key, mode] = viewMode.split(":").map((s) => s.trim());
if (key === "default") {
folderOrFileModeState = null; // ensures that no state is set
return;
}
else if (!["live", "preview", "source"].includes(mode)) {
return;
}
folderOrFileModeState = Object.assign({}, state.state);
folderOrFileModeState.mode = mode;
switch (key) {
case this.OBSIDIAN_EDITING_MODE_KEY: {
if (mode == "live") {
folderOrFileModeState.source = false;
folderOrFileModeState.mode = "source";
}
else {
folderOrFileModeState.source = true;
}
break;
}
case this.OBSIDIAN_UI_MODE_KEY:
folderOrFileModeState.source = false;
break;
}
};
for (const folderMode of this.settings.folders) {
if (folderMode.folder !== '' && folderMode.viewMode) {
const folder = this.app.vault.getAbstractFileByPath(folderMode.folder);
if (folder instanceof obsidian.TFolder) {
if (view.file.parent === folder || view.file.parent.path.startsWith(folder.path)) {
if (!state.state) { // just to be on the safe side
continue;
}
setFolderOrFileModeState(folderMode.viewMode);
}
}
else {
console.warn(`ForceViewMode: Folder ${folderMode.folder} does not exist or is not a folder.`);
}
}
}
for (const { filePattern, viewMode } of this.settings.files) {
if (!filePattern || !viewMode) {
continue;
}
if (!state.state) {
// just to be on the safe side
continue;
}
if (!view.file.basename.match(filePattern)) {
continue;
}
setFolderOrFileModeState(viewMode);
}
if (folderOrFileModeState) {
if (state.state.mode !== folderOrFileModeState.mode ||
state.state.source !== folderOrFileModeState.source) {
state.state.mode = folderOrFileModeState.mode;
state.state.source = folderOrFileModeState.source;
yield leaf.setViewState(state);
}
return;
}
// ... get frontmatter data and search for a key indicating the desired view mode
// and when the given key is present ... set it to the declared mode
const fileCache = this.app.metadataCache.getFileCache(view.file);
const fileDeclaredUIMode = fileCache !== null && fileCache.frontmatter
? fileCache.frontmatter[this.OBSIDIAN_UI_MODE_KEY]
: null;
const fileDeclaredEditingMode = fileCache !== null && fileCache.frontmatter
? fileCache.frontmatter[this.OBSIDIAN_EDITING_MODE_KEY]
: null;
if (fileDeclaredUIMode) {
if (["source", "preview", "live"].includes(fileDeclaredUIMode) &&
view.getMode() !== fileDeclaredUIMode) {
state.state.mode = fileDeclaredUIMode;
}
}
if (fileDeclaredEditingMode) {
const shouldBeSourceMode = fileDeclaredEditingMode == 'source';
if (["source", "live"].includes(fileDeclaredEditingMode)) {
state.state.source = shouldBeSourceMode;
}
}
if (fileDeclaredUIMode || fileDeclaredEditingMode) {
yield leaf.setViewState(state);
if (true == this.settings.ignoreOpenFiles) {
this.openedFiles = resetOpenedNotes(this.app);
}
return;
}
const defaultViewMode = this.app.vault.config.defaultViewMode
? this.app.vault.config.defaultViewMode
: "source";
const defaultEditingModeIsLivePreview = this.app.vault.config.livePreview === undefined ? true : this.app.vault.config.livePreview;
if (!this.settings.ignoreForceViewAll) {
let state = leaf.getViewState();
if (view.getMode() !== defaultViewMode) {
state.state.mode = defaultViewMode;
}
state.state.source = defaultEditingModeIsLivePreview ? false : true;
yield leaf.setViewState(state);
this.openedFiles = resetOpenedNotes(this.app);
}
return;
});
// "active-leaf-change": open note, navigate to note -> will check whether
// the view mode needs to be set; default view mode setting is ignored.
this.registerEvent(this.app.workspace.on("active-leaf-change", this.settings.debounceTimeout === 0
? readViewModeFromFrontmatterAndToggle
: obsidian.debounce(readViewModeFromFrontmatterAndToggle, this.settings.debounceTimeout)));
});
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settings);
});
}
onunload() {
return __awaiter(this, void 0, void 0, function* () {
this.openedFiles = [];
});
}
}
function alreadyOpen(currFile, openedFiles) {
const leavesWithSameNote = [];
if (currFile == null) {
return false;
}
openedFiles.forEach((openedFile) => {
if (openedFile == currFile.basename) {
leavesWithSameNote.push(openedFile);
}
});
return leavesWithSameNote.length != 0;
}
function resetOpenedNotes(app) {
let openedFiles = [];
app.workspace.iterateAllLeaves((leaf) => {
var _a, _b;
let view = leaf.view instanceof obsidian.MarkdownView ? leaf.view : null;
if (null === view) {
return;
}
openedFiles.push((_b = (_a = leaf.view) === null || _a === void 0 ? void 0 : _a.file) === null || _b === void 0 ? void 0 : _b.basename);
});
return openedFiles;
}
class ViewModeByFrontmatterSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
let { containerEl } = this;
containerEl.empty();
const createHeader = (text) => containerEl.createEl("h2", { text });
const desc = document.createDocumentFragment();
desc.append("Changing the view mode can be done through the key ", desc.createEl("code", { text: "obsidianUIMode" }), ", which can have the value ", desc.createEl("code", { text: "source" }), " or ", desc.createEl("code", { text: "preview" }), ".", desc.createEl("br"), "Changing the editing mode happens by declaring the key ", desc.createEl("code", { text: "obsidianEditingMode" }), "; it takes ", desc.createEl("code", { text: "live" }), " or ", desc.createEl("code", { text: "source" }), " as value.");
new obsidian.Setting(this.containerEl).setDesc(desc);
new obsidian.Setting(containerEl)
.setName("Ignore opened files")
.setDesc("Never change the view mode on a note which was already open.")
.addToggle((checkbox) => checkbox
.setValue(this.plugin.settings.ignoreOpenFiles)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.ignoreOpenFiles = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName("Ignore force view when not in frontmatter")
.setDesc("Never change the view mode on a note that was opened from another one in a certain view mode")
.addToggle((checkbox) => {
checkbox
.setValue(this.plugin.settings.ignoreForceViewAll)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.ignoreForceViewAll = value;
yield this.plugin.saveSettings();
}));
});
new obsidian.Setting(containerEl)
.setName("Debounce timeout in milliseconds")
.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.`)
.addText((cb) => {
cb.setValue(String(this.plugin.settings.debounceTimeout)).onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.debounceTimeout = Number(value);
yield this.plugin.saveSettings();
}));
});
const modes = [
"default",
"obsidianUIMode: preview",
"obsidianUIMode: source",
"obsidianEditingMode: live",
"obsidianEditingMode: source",
];
createHeader("Folders");
const folderDesc = document.createDocumentFragment();
folderDesc.append("Specify a view mode for notes in a given folder.", folderDesc.createEl("br"), "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.", folderDesc.createEl("br"), "Precedence is from bottom (highest) to top (lowest), so if you have child folders specified, make sure to put them below their parent folder.");
new obsidian.Setting(this.containerEl).setDesc(folderDesc);
new obsidian.Setting(this.containerEl)
.setDesc("Add new folder")
.addButton((button) => {
button
.setTooltip("Add another folder to the list")
.setButtonText("+")
.setCta()
.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.folders.push({
folder: "",
viewMode: "",
});
yield this.plugin.saveSettings();
this.display();
}));
});
this.plugin.settings.folders.forEach((folderMode, index) => {
const div = containerEl.createEl("div");
div.addClass("force-view-mode-div");
div.addClass("force-view-mode-folder");
const s = new obsidian.Setting(this.containerEl)
.addSearch((cb) => {
cb.setPlaceholder("Example: folder1/templates")
.setValue(folderMode.folder)
.onChange((newFolder) => __awaiter(this, void 0, void 0, function* () {
if (newFolder &&
this.plugin.settings.folders.some((e) => e.folder == newFolder)) {
console.error("ForceViewMode: This folder already has a template associated with", newFolder);
return;
}
this.plugin.settings.folders[index].folder = newFolder;
yield this.plugin.saveSettings();
}));
})
.addDropdown(cb => {
modes.forEach(mode => {
cb.addOption(mode, mode);
});
cb.setValue(folderMode.viewMode || "default")
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.folders[index].viewMode = value;
yield this.plugin.saveSettings();
}));
})
.addExtraButton((cb) => {
cb.setIcon("cross")
.setTooltip("Delete")
.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.folders.splice(index, 1);
yield this.plugin.saveSettings();
this.display();
}));
});
s.infoEl.remove();
div.appendChild(containerEl.lastChild);
});
createHeader("Files");
const filesDesc = document.createDocumentFragment();
filesDesc.append("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\"", filesDesc.createEl("br"), "Note that this will force the view mode, even if it have a different view mode set in its frontmatter.", filesDesc.createEl("br"), "Precedence is from bottom (highest) to top (lowest).", filesDesc.createEl("br"), "Notice that configuring a file pattern will override the folder configuration for the same file.");
new obsidian.Setting(this.containerEl).setDesc(filesDesc);
new obsidian.Setting(this.containerEl)
.setDesc("Add new file")
.addButton((button) => {
button
.setTooltip("Add another file to the list")
.setButtonText("+")
.setCta()
.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.files.push({
filePattern: "",
viewMode: "",
});
yield this.plugin.saveSettings();
this.display();
}));
});
this.plugin.settings.files.forEach((file, index) => {
const div = containerEl.createEl("div");
div.addClass("force-view-mode-div");
div.addClass("force-view-mode-folder");
const s = new obsidian.Setting(this.containerEl)
.addSearch((cb) => {
cb.setPlaceholder(`Example: " - All$" or "1900-01")`)
.setValue(file.filePattern)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
if (value &&
this.plugin.settings.files.some((e) => e.filePattern == value)) {
console.error("ForceViewMode: Pattern already exists", value);
return;
}
this.plugin.settings.files[index].filePattern = value;
yield this.plugin.saveSettings();
}));
})
.addDropdown((cb) => {
modes.forEach((mode) => {
cb.addOption(mode, mode);
});
cb.setValue(file.viewMode || "default").onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.files[index].viewMode = value;
yield this.plugin.saveSettings();
}));
})
.addExtraButton((cb) => {
cb.setIcon("cross")
.setTooltip("Delete")
.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.files.splice(index, 1);
yield this.plugin.saveSettings();
this.display();
}));
});
s.infoEl.remove();
div.appendChild(containerEl.lastChild);
});
}
}
module.exports = ViewModeByFrontmatterPlugin;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsIm1haW4udHMiXSwic291cmNlc0NvbnRlbnQiOm51bGwsIm5hbWVzIjpbIlBsdWdpbiIsIk1hcmtkb3duVmlldyIsIlRGb2xkZXIiLCJkZWJvdW5jZSIsIlBsdWdpblNldHRpbmdUYWIiLCJTZXR0aW5nIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBb0dBO0FBQ08sU0FBUyxTQUFTLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFO0FBQzdELElBQUksU0FBUyxLQUFLLENBQUMsS0FBSyxFQUFFLEVBQUUsT0FBTyxLQUFLLFlBQVksQ0FBQyxHQUFHLEtBQUssR0FBRyxJQUFJLENBQUMsQ0FBQyxVQUFVLE9BQU8sRUFBRSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFO0FBQ2hILElBQUksT0FBTyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEVBQUUsVUFBVSxPQUFPLEVBQUUsTUFBTSxFQUFFO0FBQy9ELFFBQVEsU0FBUyxTQUFTLENBQUMsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtBQUNuRyxRQUFRLFNBQVMsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENB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