as I like the tags selected from the metadata part, i wanted to keep that
the following script offers choices: 1) add any ad hoc properties including tags and 2) the other way: multiple properties through modal forms
the first part includes tag adding as well as normal properties: when user types in tags
(plural), the suggester from metadatacache pops up and the user is prompted to add more tags not used before (hence not in the cache)
the second part, if picked from suggester, is handled by the user-defined modal form, where you can add multiple props
the script can be used for file creation as well now – yaml is generated for empty file by Obsidian’s processFrontmatter
the script uses linter so users need linter installed and set up for yaml handling (beneficial)
<%*
(async () => {
// Function to execute the linter command after a delay
const executeLinter = async () => {
await new Promise(resolve => setTimeout(resolve, 350));
await app.commands.executeCommandById("obsidian-linter:lint-file-unless-ignored");
};
// Function to update the front matter with new data
const updateFrontMatter = async (propertyName, propertyValues) => {
await app.fileManager.processFrontMatter(tp.config.target_file, async (frontmatter) => {
if (!frontmatter[propertyName]) {
frontmatter[propertyName] = propertyValues.length === 1 ? propertyValues[0] : propertyValues;
} else {
if (Array.isArray(frontmatter[propertyName])) {
frontmatter[propertyName] = [...new Set([...frontmatter[propertyName], ...propertyValues])];
} else {
frontmatter[propertyName] = propertyValues.length === 1 ? propertyValues[0] : propertyValues;
}
}
});
};
// Function to remove double quotes from around boolean values (true and false) in the YAML block
const removeBooleanQuotes = async () => {
const currentFile = app.workspace.getActiveFile();
let fileContent = await app.vault.read(currentFile);
fileContent = fileContent.replace(/^---([\s\S]*?)---/gm, (match, p1) => {
return match.replace(/"true"|'true'|"false"|'false'/g, match => match.replace(/["']/g, ''));
});
await app.vault.modify(currentFile, fileContent);
};
// Prompt user for the type of input
const userInput = await tp.system.suggester(
["Add/Modify Properties", "Add Modal Form Props"],
["Add/Modify Properties", "Add Modal Form Props"],
false,
"Choose an option:"
);
if (!userInput) {
return;
}
if (userInput === "Add/Modify Properties") {
let isAddingProperties = true;
while (isAddingProperties) {
// Prompt user for the property name
const propertyName = await tp.system.prompt("Enter the property name:");
if (!propertyName) {
console.log("No property name provided. Exiting script.");
isAddingProperties = false;
break;
}
console.log("Property name provided:", propertyName);
if (propertyName.toLowerCase() === "tags") {
let selectedTags = [];
// Fetch existing tags
const dv = app.plugins.plugins.dataview.api;
const cachedTags = Object.entries(app.metadataCache.getTags())
.sort((a, b) => b[1] - a[1]); // Sorted by frequency
let selectMore = true;
while (selectMore) {
let choice = await tp.system.suggester((t) => t[0] + " (" + t[1] + ")", cachedTags, false, "[Select some tags. (ESC or click out of box on mobile when finished)]");
if (!choice || !choice[0]) {
selectMore = false;
} else {
selectedTags.push(choice[0]);
}
}
console.log("Selected tags:", selectedTags);
let isAddingTags = true;
while (isAddingTags) {
const userTags = await tp.system.prompt("Add (more) tags until ESC.");
if (userTags === null || userTags === "") {
isAddingTags = false;
} else {
selectedTags.push(...userTags.split(',').map(tag => tag.trim()));
}
}
// Remove hashes from tags
selectedTags = selectedTags.map(tag => tag.replace(/#/g, ''));
// Update the front matter with the new tags
await updateFrontMatter("tags", selectedTags);
} else {
let propertyValues = [];
let isBooleanProperty = false;
let isAddingValues = true;
while (isAddingValues) {
// Prompt user for a value
const propertyValue = await tp.system.prompt(`Enter the value for ${propertyName}:`);
if (!propertyValue) {
console.log("No property value provided. Exiting script.");
isAddingValues = false;
break;
}
console.log("Property value provided:", propertyValue);
propertyValues.push(propertyValue);
// Check if the value is a boolean
if (propertyValue.toLowerCase() === "true" || propertyValue.toLowerCase() === "false") {
isBooleanProperty = true;
isAddingValues = false;
} else {
// Ask if the user wants to add another value
const addAnotherValue = await tp.system.prompt(`Do you want to add another value for ${propertyName}? (yes or no)`);
if (addAnotherValue.toLowerCase() !== 'yes') {
isAddingValues = false;
}
}
}
// Update the front matter with the new property
await updateFrontMatter(propertyName, propertyValues);
// If a boolean value was added, remove double quotes around boolean values in YAML
if (isBooleanProperty) {
await removeBooleanQuotes();
}
}
// Ask if the user wants to add another property
const addAnother = await tp.system.prompt("Do you want to add another property? (yes or no)");
if (addAnother.toLowerCase() !== 'yes') {
isAddingProperties = false;
}
}
await executeLinter();
} else if (userInput === "Add Modal Form Props") {
const modalForm = app.plugins.plugins.modalforms.api;
const run = async (frontmatter) => {
const result = await modalForm.openForm('userdefinedform', {
values: { ...frontmatter },
});
return result.getData();
};
// Get the data from the form
const data = await run(tp.frontmatter);
if (!data) {
return;
}
// Update the frontmatter with the new data
await app.fileManager.processFrontMatter(tp.config.target_file, frontmatter => {
Object.assign(frontmatter, data);
});
await executeLinter();
}
})();
%>
again, userdefinedform
is to be replaced with the id-name of the form you have set up
but you can use the script without adding modal forms at all
also, here I removed the hashes from tags and mobile check (metadatacache option is enabled for mobile now as well)
also, the script could be streamlined but this is good enough now i think
i think the modal form can be set up to everything fast but it cannot be used to pick tags from the cache, hence the two-part method