Quickadd append into properties

I use Quickadd with templater to create new notes from templates and append the link at the cursor position into the original note. This is very useful for creating references to new notes without leaving the note I am working on.
It worked perfectly with inline fildes, the link was inserted at the position of the cursor.

I recently migrated from inline fields to the now standard YAML properties. The problem is that the append function of Quickadd doesn’t insert the link at the position of the cursor when I am in a property field. Instead it creates the link at the first line after the YAML block.

Have you some ideas how I could reproduce this workflow (creating from a property field and inserting the link to the new note in the property field at the cursor position)?

Hi, are you using tp.file.cursor()? I had the exact same problem that it just would not be written in the properties. I gave up and added it as an inline field.
Sorry this is not help, but at least you know you are not alone, lol

Thanks for your answer. At least I know that I’m not alone to have this problem and it is not due to a misunderstanding of the quickadd plugin.
I can’t use the tp.cursor since I need ti be able to insert a link in different properties in my template.

Should you find a workaround, please let me know!

from various sources mostly from this forum (sorry, i cannot give credit to whom i am most indebted) i created a non-quickadd pure templater script anyone can use to add properties on demand into a file that already has yaml and some props

i am using metadatacache way to add tags only on desktop as dataview is not working for my vault on mobile so if you want to remove the if (!app.isMobile) { condition, some ai robot can help
obviously install and enable dataview on desktop for this to work for you

tags are chosen for default but you can delete back and add your own prop name

currently only tags and topic properties allow multiple values so if the user needs a different property, replace the topic word in the script with any other to be able to add multiple values to that one
you can ask ai to make a script based on this to add other props with multi values of course…

otherwise the rule is: one new prop > one value taken only
if you add e.g. ‘chickencrossedtheRoad’ > ‘false’ or ‘true’, correct boolean values will be added without quotes

<%*
(async () => {
    // Prompt user for property name
    const propertyName = await tp.system.prompt("Enter the property name (tags, topic or any other property, including a new one):", "tags", "tags");
    if (!propertyName) {
        console.log("No property name provided. Exiting script.");
        return;
    }
    console.log("Property name provided:", propertyName);

    let propertyValues = [];

    if (propertyName === "topic") {
        let isAddingTopic = true;
        while (isAddingTopic) {
            const topic = await tp.system.prompt("Add topic until ESC or close box on mobile to finish (if you add links, no need to add double quotes).");
            if (topic === null || topic === "") {
                isAddingTopic = false;
            } else {
                propertyValues.push(topic);
            }
        }
    } else if (propertyName === "tags") {
        let selectedTags = [];

        if (!app.isMobile) {
            // 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 {
                // Split the userTags string into an array of tags, trim each tag, prepend #, and push to selectedTags
                userTags.split(',').forEach(tag => {
                    const trimmedTag = tag.trim();
                    if (trimmedTag) { // Check if the tag is not empty after trimming
                        selectedTags.push('#' + trimmedTag); // Prepend # here
                    }
                });
            }
        }
        propertyValues = selectedTags;
    } else {
        // Prompt user for a single value
        const propertyValue = await tp.system.prompt(`Enter the value for ${propertyName}:`);
        if (!propertyValue) {
            console.log("No property value provided. Exiting script.");
            return;
        }
        console.log("Property value provided:", propertyValue);
        propertyValues.push(propertyValue);
    }

    // Update front matter using Obsidian API
    await app.fileManager.processFrontMatter(tp.config.target_file, 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;
            }
        }
        console.log("Frontmatter updated");
    });

    // Wait for the update to complete
    await new Promise(resolve => setTimeout(resolve, 250));

    // Remove double quotes from around boolean values (true and false)
    const currentFile = app.workspace.getActiveFile();
    let fileContent = await app.vault.read(currentFile);
    // Match YAML block and replace double quotes around true and false
    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);
})();
-%>

tags values are added hashes because 1. the metadatacache selection adds them 2. you can use your files anywhere else to search your tags

related thread:

so needed plugins:

  • templater
  • dataview

copy contents to file and add that filename to templater to use the script
fire script on active note

3 Likes

you can use a different method

install modal forms plugin

make your own form
assign checkbox (true/false) items to toggle, multiple selections (tick unknown values option there), etc.

save the form

then make a templater script:

<%*
const modalForm = app.plugins.plugins.modalforms.api; 

const run = async (frontmatter) => {
  const result = await modalForm.openForm('userdefinedform', {
    values: { ...frontmatter },
  });
  return result.getData();
};

// First we get the data from the form
const data = await run(tp.frontmatter);

// Then we update the frontmatter with the new data
app.fileManager.processFrontMatter(
  tp.config.target_file,
  frontmatter => {
    Object.assign(frontmatter, data);
  },
);
%>

replace userdefinedform with the name of your form, e.g. people_form, etc.

the new properties will be put in end position above the closing --- of yaml

linter plugin can help put the props in predefined order

so you can add to the end of the script:

// Use Linter to put the added props in predefined order
(async () => {
  await new Promise(resolve => setTimeout(resolve, 350));
  await app.commands.executeCommandById("obsidian-linter:lint-file-unless-ignored");
})();

you can increase 350 if you have long files or a large vault i guess

Thansk Yurcee, I’m going to test that.
It seems a bit complicated but if it works for me, it’s great.

try the modal forms, it’s great

takes the coding part off of your shoulders if you want to add extra props to your code over time…
otherwise in coding you would need to add if else parts, look out for indentation issues, etc.

linter may be intimidating but worth using it – second setting tab is yaml where you look for
image
add your usual properties so there is consistency of props order across the vault

current code uses Obsidian API to add props into files already with yaml

otherwise developer of modal forms usually gives examples of adding to files with no yaml – so ignore those and use my code – well, not mine, i took some example of danielo’s and tweaked it

1 Like

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

turns out btw, the tag picker can be used with the modal form/forms plugin

add multiselect and dataview, tick unknown values
name the property tags

in the dataview query box, enter:

[...new Set(dv.pages("").flatMap(p => p.file.tags || []))]

you can sort tags alphabetically:
[...new Set(dv.pages("").flatMap(p => p.file.tags || []))].sort((a, b) => a.localeCompare(b))

sort by frequency (slower):
[...new Set(dv.pages("").flatMap(p => p.file.tags || []))].map(tag => [tag, dv.pages("").flatMap(p => p.file.tags || []).filter(t => t === tag).length]).sort((a, b) => b[1] - a[1]).map(t => t[0])

if you add the second tag, the drop-down menu may not appear (bug?), but keep writing: the tags will appear as if by fuzzy search

you can add your new tags not in the cache as well (if you tick unknown values)

unfortunately, there doesn’t seem to be a way to add default values in the graphical user interface but you can do it in the templater code:

const result = await modalForm.openForm('add_your_form_file', {
      values: { ...frontmatter, tags: ["somedefaultvalue"] },