DataviewJS Snippet Showcase

I believe you need to use == for comparisons in js, so it would be p.date == dv.date

1 Like

I was wondering with DataviewJS could we take things one step further than this example:

   dv.header(2, 'Research Tasks');
   dv.taskList(dv.pages('#research').file.tasks
    .where(t => !t.completed));

That will comb through pages with the tag ā€œ#researchā€ and list out any incomplete tasks.

How could we extend this? What Iā€™m trying to do is I have 3 different pages with 10 incomplete tasks among them. But what I want this to do is just grab the FIRST incomplete task from each of the 3 different pages. Basically listing for me the NEXT STEP for each #research page. Iā€™m sure this is very close and just needs that last little bit of Javascript magic to make it work.

Thanks in advance for any help provided!

tasks have a line attribute, which you can sort by and limit in order to isolate. for multiple pages you might need to test the task line number against the smallest task line number of that page, if that makes sense.

It does not make sense, yet. But it definitely gives me some things I can start searching about. So thank you for the clues to follow up on!

So line attribute like the line of the page where the task is located? That makes sense.

So is there a way to loop over these tasks and know their page and line? I would want the lowest line for each page for the incomplete tasks, I think.

Would that be an altered .where() or would I have to go back to .file and look for the lines? I guess Iā€™m looking for the collection to loop (or foreach) over and then figure out how to define the criteria or filter to apply to each iteration.

Seems to me. :slight_smile:

this sort of does the job:

TABLE file.tasks[0].text as task
FROM #your_tag
WHERE file.tasks
LIMIT 10

replace the from tag with whatever tag unifies your notes, or use some other source/add your filtering to the where block. Limit is just there in case of a mistake to prevent it blowing up, can be deleted.

One thing that occurs to me is it wouldnā€™t filter out completed tasks. js is probably required for a more complete solution. I might try later if I get some time.

as usual, the problem bothers me until I can solve it.


// grab first non-completed task from #test pages
let ftasks = dv.pages("#test").map(t => t.file.tasks.where(t => t.completed==false).sort(t.line).first() ) 

// filter undefined
let first_tasks = ftasks.filter(item => item !== undefined)

// render result
dv.taskList(first_tasks)
1 Like

Hey! That looks like a winner!

Hi, Iā€™m trying to retrive all tasks which contains a due date of today or upcoming. The date are written as part of the task text like in: - [ ] Review paper on Hunger [[2023.02.27]]
Iā€™m trying this one, but the result is always empty. Can anybody help?

dv.taskList(dv.pages().file.tasks 
  .where(t => !t.completed)
  .where(t => t.text.includes(dv.date('today'), 'YYYY-MM-DD')))

Iā€™ve written a new version of the commands and hotkey script, see here, and with that you could also achieve the same by changing the following at the top:

const showDefaultKeys = false
const showCustomKeys = true
const showCommandsWithoutKeys = false

Itā€™ll also show any duplicates youā€™ve set in your hotkeys, if there are any.

Iā€™ve revised the script, see here, and now itā€™s default settings is showing the module/plugin in a separate column.

If you want to, you can also play around with the setting of showAllModules, and changing which modules/plugins you want to display or exclude from the list. Iā€™ve listed in a comment section all the various modules related to Obsidian, so if you want to exclude them thatā€™s easily done. Other plugins can be handled by viewing the list, and then adding them to either list of choice.

Hope this helps in your specific use case.

Thanks for this, really useful to me. I made one change to my version to add a link to the Obsidian search for each of the entries. Useful for cleanup for me.
I changed the last map line to:

	.map(k => ["["+k.fieldName+"](obsidian://search?query="+k.fieldName+")",k.count]));
1 Like

DVJS creates a table that shows the headings and the links below each heading


Sample note:


[[Mathematical foundations]]

[[Coding theory]] ā€“ Useful in networking, programming, system development, and other areas where computers communicate with each other.

[[Game theory]] - Useful in artificial intelligence and cybernetics.

[[Discrete Mathematics]]

[[Graph theory]] ā€“ Foundations for data structures and searching algorithms.

[[Mathematical logic]] ā€“ Boolean logic and other ways of modeling logical queries; the uses and limitations of formal proof methods

[[Number theory]] ā€“ Theory of the integers. Used in cryptography as well as a test domain in artificial intelligence.

[[Algorithms and data structures]]

[[Algorithms]] ā€“ Sequential and parallel computational procedures for solving a wide range of problems.

[[Data structures]] ā€“ The organization and manipulation of data.



DVJS code:


// Read the current file
const input = await dv.io.load(dv.current().file.path);

// Initialize the table array
const table = [];

// Split the input file into lines
const lines = input.split('\n');

let currentSection = null;
let currentSectionIndex = -1;
for (const line of lines) {
  const match = line.match(/^#+\s+(\[\[.*?\]\])/);
  if (match) {
    // This is a new section
    currentSection = match[1];
    currentSectionIndex++;
    table[currentSectionIndex] = [currentSection];
  } else {
    const match = line.match(/^\s*(\[\[.*?\]\])/);
    if (match) {
      // This is a sub-item of the current section
      if (currentSection !== null) {
        if (table[currentSectionIndex].length === 1) {
          table[currentSectionIndex][1] = [];
        }
        table[currentSectionIndex][1].push(match[1]);
      }
    }
  }
}

//console.log(table);
dv.table(["Topic", "Sub-topic"], table)


Result:


IMPORTANT:

  1. ā€œTopicā€ (headings) can be at any level (ie. #, ##, ### etcā€¦)
  2. ā€œSub-topicā€ links must be at the beginning of each line without any preceding character
  3. Both ā€œTopicā€ and ā€œSub-topicā€ must be links (it also accepts links to notes that donā€™t exist yet

Pros
No longer need to use tasksā€™ notation - [ ] in order to grab elements (if are links themselves) (ie. Sub-Topic) below headings (ie. Topic)


#get-help #share-showcase

1 Like

Is there a way to use customJS with inline dataviewjs? I am working on adding data summaries to my daily note and wanted to store all of the methods in a customJS class and use this class to generate the Talley?

For example is there a way to use the defined class (ie const {DataHelper} = customJS which I put in a dataviewjs code block at the top of the note, later in the same note inline

const {DataHelper} = customJS

ADDITIONAL TEXT OF NOTE

Coffee: $= DataHelper.sumFunction(ā€˜coffeeā€™)

When I do this, I receive an error that DataHelper is undefined. Curious if there is a way to ā€˜persistā€™ a variable defined in on dataviewjs code block throughout a given note?

Thanks in advance for any clarity on how to do this, or if it is even possible.

Don

So the notation of const {DataHelper} = customJS is really just a shortcut for storing the customJS.DataHelper into a variable of its own. Knowing that, we can redefine to not use this extraction at all and simply do:

Coffee:: `$= customJS.DataHelper.sumFunction('coffee') `

Some other variants which also should work, but personally I donā€™t think looks as nice:

`$= const DH = customJS.DataHelper; DH.sumFunction('coffee') `
`$= const {DataHelper} = customJs; DataHelper.sumFunction('coffee') `
`$= const {DataHelper:DH} = customJS; DH.sumFunction('coffee') `

For more information on how to destructure objects, see Destructuring assignment - JavaScript | MDN

Also note that as long as you can use semicolon to indicate the line shift, you can have as many ā€œlinesā€ of code within the inline dataviewjs query as you feel like.

One final note: When you do coffee:: `$= ā€¦ `, you donā€™t set the inline field to the value of that inline query, you set the field to the query itself, so if you use this in another context it can cause issues if thatā€™s not a place where that query can be evaluated.

For example, if you use the first variant above, and then do `$= console.log(dv.current().coffee) `, the output youā€™ll get in the console is `$= customJS.DataHelper.sumFunction(ā€˜coffeeā€™) `.

Thank you very much for that clarification. I appreciate the details and now understand how curly brackets work within the context of customJS! Thanks again for this

Don

1 Like

Amazing code, just tried it.
Is there a way to only pull in the commands that have a hotkey?

when I run the snippets, I got an error like:

Evaluation Error: TypeError: Cannot read properties of undefined (reading ā€˜toStringā€™)
at eval (eval at (plugin:dataview), :122:67)
at DataviewInlineApi.eval (plugin:dataview:18370:16)
at evalInContext (plugin:dataview:18371:7)
at asyncEvalInContext (plugin:dataview:18381:32)
at DataviewJSRenderer.render (plugin:dataview:18402:19)
at DataviewJSRenderer.onload (plugin:dataview:17986:14)
at e.load (app://obsidian.md/app.js:1:869278)
at DataviewApi.executeJs (plugin:dataview:18921:18)
at DataviewPlugin.dataviewjs (plugin:dataview:19423:18)
at eval (plugin:dataview:19344:124)

how to fix it?

I have a ā€œhackā€ that I created to allow me to update frontmatter using dvjs and meta-bind. I store this in a file called client-menu.js that I access from a template via dv.view.
image

// Script to find all clients from frontmatter (2 flavours)
let clients1 = dv.pages().file.frontmatter.clients.values; // where 'clients' is populated in frontmatter YAML
clients1.unshift("ALL"); // add "ALL" option because I always want that, at top
let clients2 = dv.pages().file.where(function(b){if(Array.isArray(b.frontmatter.type)){if (b.frontmatter.type.includes("client")){return true;}}return (b.frontmatter.type == "client")? true : false;}).name; // where a note frontmatter type = 'client', had to handle empty & non-array
let uniqueclients = [...new Set([...clients1,...clients2])]; // concatenate & dedupe via a Set
let optionlist = [];
// nasty code to build meta-bind code block...but it works!
uniqueclients.forEach((currentElement)=>{optionlist.push("option(" + currentElement + ")")});
dv.paragraph("```meta-bind\nINPUT[multi_select("+optionlist+"):clients]\n```")

If anyone has a cleaner way to do this Iā€™d love to hear it!

First off, thanks for this snippet! It has saved me tons of timeā€¦but I had a problem with it being really slow, so I modified the script to add in memory caching of the command list since that isnā€™t something that changes very often. This prevents it from having the super slow load time when you have alot of commands where the commands wouldnā€™t show up for a few seconds when I folded and unfolded the header where the dataview query was nested under. This also occurs when you go to a new tab and come back.

The optimized version that includes the in memory caching code, fixes this issue and it will only reload the query and pull data from the Obsidian command list every 10 minutes.

This is the code.

function Cache(config) {
    config = config || {};
    config.trim = config.trim || 600;
    config.ttl = config.ttl || 3600;

    var data = {};
    var self = this;

    var now = function() {
        return new Date().getTime() / 1000;
    };

    /**
     * Object for holding a value and an expiration time
     * @param expires the expiry time as a UNIX timestamp
     * @param value the value of the cache entry
     * @constructor ĀÆ\(Ā°_o)/ĀÆ
     */
    var CacheEntry = function(expires, value) {
        this.expires = expires;
        this.value = value;
    };

    /**
     * Creates a new cache entry with the current time + ttl as the expiry.
     * @param value the value to set in the entry
     * @returns {CacheEntry} the cache entry object
     */
    CacheEntry.create = function(value) {
        return new CacheEntry(now() + config.ttl, value);
    };

    /**
     * Returns an Array of all currently set keys.
     * @returns {Array} cache keys
     */
    this.keys = function() {
        var keys = [];
        for(var key in data)
            if (data.hasOwnProperty(key))
                keys.push(key);
        return keys;
    };

    /**
     * Checks if a key is currently set in the cache.
     * @param key the key to look for
     * @returns {boolean} true if set, false otherwise
     */
    this.has = function(key) {
        return data.hasOwnProperty(key);
    };

    /**
     * Clears all cache entries.
     */
    this.clear = function() {
        for(var key in data)
            if (data.hasOwnProperty(key))
                self.remove(key);
    };

    /**
     * Gets the cache entry for the given key.
     * @param key the cache key
     * @returns {*} the cache entry if set, or undefined otherwise
     */
    this.get = function(key) {
        return data[key].value;
    };

    /**
     * Returns the cache entry if set, or a default value otherwise.
     * @param key the key to retrieve
     * @param def the default value to return if unset
     * @returns {*} the cache entry if set, or the default value provided.
     */
    this.getOrDefault = function(key, def) {
        return self.has(key) ? data[key].value : def;
    };

	/**
	 * TODO: Add JSDoc to this function
	 */
	this.getOrCreate = function(key, fn) {
        if (self.has(key)) {
            return self.get(key);
        }
        const value = fn();
        self.set(key, value);
        return value;
    }
    
    /**
     * Sets a cache entry with the provided key and value.
     * @param key the key to set
     * @param value the value to set
     */
    this.set = function(key, value) {
        data[key] = CacheEntry.create(value);
    };

    /**
     * Removes the cache entry for the given key.
     * @param key the key to remove
     */
    this.remove = function(key) {
        delete data[key];
    };

    /**
     * Checks if the cache entry has expired.
     * @param entrytime the cache entry expiry time
     * @param curr (optional) the current time
     * @returns {boolean} true if expired, false otherwise
     */
    this.expired = function(entrytime, curr) {
        if(!curr)
            curr = now();
        return entrytime < curr;
    };

    /**
     * Trims the cache of expired keys. This function is run periodically (see config.ttl).
     */
    this.trim = function() {
        var curr = now();
        for (var key in data)
            if (data.hasOwnProperty(key))
                if(self.expired(data[key].expires, curr))
                    self.remove(key);
    };

    // Periodical cleanup
    setInterval(this.trim, config['trim'] * 1000);

    //--------------------------------------------------------
    // Events

    var eventCallbacks = {};

    this.on = function(event, callback) {
        // TODO handle event callbacks
    };
}
const cache = new Cache({ trim: 600, ttl: 3600 });

const getNestedObject = (nestedObj, pathArr) => {
    const cacheKey = `nestedObject-${JSON.stringify({ nestedObj, pathArr })}`;
    return cache.getOrCreate(cacheKey, () => {
        return pathArr.reduce((obj, key) =>
            (obj && obj[key] !== 'undefined') ? obj[key] : undefined, nestedObj);
    });
}

function hilite(keys, how) {
    if (keys && keys[1][0] !== undefined) {
        return how + keys.flat(2).join('+').replace('Mod', 'Ctrl') + how;
    } else {
        return how + 'ā€“' + how;
    }
}

function getHotkey(arr, highlight=true) {
    const cacheKey = `hotkey-${JSON.stringify({ arr, highlight })}`;
    return cache.getOrCreate(cacheKey, () => {
        let hi = highlight ? '**' : '';
        let defkeys = arr.hotkeys ? [[getNestedObject(arr.hotkeys, [0, 'modifiers'])],
        [getNestedObject(arr.hotkeys, [0, 'key'])]] : undefined;
        let ck = app.hotkeyManager.customKeys[arr.id];
        var hotkeys = ck ? [[getNestedObject(ck, [0, 'modifiers'])], [getNestedObject(ck, [0, 'key'])]] : undefined;
        return hotkeys ? hilite(hotkeys, hi) : hilite(defkeys, '');
    });
}

let cmds = dv.array(Object.entries(app.commands.commands))
    .sort(v => v[1].name, 'asc');

dv.paragraph(cmds.length + " commands currently enabled; " +
    "non-default hotkeys <strong>bolded</strong>.<br><br>");

dv.table(["Command ID", "Name in current locale", "Hotkeys"],
  cmds.map(v => [
    v[1].id,
    v[1].name,
    getHotkey(v[1]),
    ])
);
6 Likes

Hey @Moonbase59 ! Thanks a lot for sharing this code. It has been conducive to me.
I had to make a minor change to remove the Luxon dependency, which was breaking for me.
Also, as I use a different folder structure for my daily notes, I added some code to search the pages from the daily notes configured folder. The relevant lines of code I changed:

var folder = app['internalPlugins']['plugins']['daily-notes']['instance']['options']['folder'] || dv.current().file.folder
var p = dv.pages('"'+folder+'"').where(p => p.file.day).map(p => [p.file.name, p.file.day.toISODate()]).sort(p => p[1]);
var t = dv.current().file.day ? dv.current().file.day.toISODate() : dv.date("now").toFormat("yyyy-MM-dd");

Cheers,
Gilmar