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");
Wanted a drop down menu for my dashboard so I used chat gpt to create this query and a css snippet to style it. I’m sure the query could be simplified or there is a better way to do this but it works and links to the pages AND it’s all updated automatically.
function formatDate(dateString) {
let date = new Date(dateString);
let options = { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true };
return date.toLocaleString("en-US", options);
}
let folderTree = new Map();
function addToFolderTree(path, content) {
let parts = path.split("/");
let currentLevel = folderTree;
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (!currentLevel.has(part)) {
currentLevel.set(part, { folders: new Map(), files: "" });
}
if (i === parts.length - 1) {
currentLevel.get(part).files = content;
} else {
currentLevel = currentLevel.get(part).folders;
}
}
}
for (let group of dv.pages('"----01. PROJECTS"').groupBy(p => p.file.folder)) {
let folderName = group.key;
let rows = group.rows
.map(
k => `| [${k.file.name.replace(/\|/g, "\\|")}](/${k.file.path.replace(/ /g, "%20")}) | ${formatDate(k.file.ctime)} | ${formatDate(k.file.mtime)} |`
)
.join("\n");
let tableContent = `
| Name | Created | Modified |
| ---- | ------- | -------- |
${rows}
`;
addToFolderTree(folderName, tableContent);
}
function renderFolderTree(folderMap, level = 0) {
let content = "";
for (let [folder, data] of folderMap.entries()) {
let subcontent = data.folders.size > 0 ? renderFolderTree(data.folders, level + 1) : "";
let folderContent = data.files ? data.files : "";
if (level > 0) {
content += `### ${folder}\n<details><summary>Click to expand</summary>\n\n${subcontent}${folderContent}\n\n</details>\n`;
} else {
content += `${subcontent}${folderContent}`;
}
}
return content;
}
dv.header(6, renderFolderTree(folderTree));
SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG:
I scrolled through a fair amount of this thread looking for a way to list ‘todo’ callouts with a link, but only in a specific folder with a specified tag. I could not find it, so I am posting this for posterity’s sake. Credit to this question thread which allowed me to form a base to build this.
```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')
//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
const regex = />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/
//check for more types of callouts by adding new const regex# where # is a number
const regex2 = />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/
const rows = []
for (const page of pages) {
const file = app.vault.getAbstractFileByPath(page.file.path);
//get file path as string
var checkMe = "" + page.file.path;
// Read the file contents
const contents = await app.vault.read(file)
// Extract the summary via regex
for (const callout of contents.match(new RegExp(regex, 'sg')) || []) {
//check if the file path is in your designated file path.
//change FILEPATH to any keyword, folder name, or file path
//You can also change to exclude a file path by changing "==true" to "!=true"
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex, 's'))
rows.push([match[1], page.file.link])
}
}
//duplicate the for loop for each const regex# you created
//adjust regex to the appropriate name
for (const callout of contents.match(new RegExp(regex2, 'sg')) || []) {
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex2, 's'))
rows.push([match[1], page.file.link])
}
}
}
dv.table(['Term', 'Link'], rows)
```
I am still playing with this, and if I update it, I will edit this post or reply to it.
Hope this helps someone.
SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG (UPDATE):
I went and tried to edit the post I made to include the regex Array, but I was unable to do so.
```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')
//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
//check for more types of callouts by adding new entries in the array.
//Remember: every key in the Array has to have a comma at the end, except for the last one
const regex = {
0: />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/,
1: />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/
}
const rows = []
for (const page of pages) {
const file = app.vault.getAbstractFileByPath(page.file.path);
//get file path as string
var checkMe = "" + page.file.path;
// Read the file contents
const contents = await app.vault.read(file)
//cycle through the callout type array.
//Be sure to change the 'i<2' to be one more than your highest array key.
for(var i = 0; i < 2; i++){
// Extract the summary via regex
for (const callout of contents.match(new RegExp(regex[i], 'sg')) || []) {
//check if the file path is in your designated file path.
//change FILEPATH to any keyword, folder name, or file path
//You can also change to exclude a file path by changing "==true" to "!=true"
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex[i], 's'))
rows.push([match[1], page.file.link])
}
}
}
}
dv.table(['Term', 'Link'], rows);
```
Not the OP, but the following works in a local diagnostic (see screenshot below).
Person File:
---
birthday: 2021-08-17
tags: family
---
Birthdays File
---
searchterm: '#family'
duration: 1 year
---
```dataviewjs
var start = moment().startOf('day');
var end = moment(start).add(dv.current().duration);
var dateformat = "YYYY-MM-DD";
if (dv.current().dateformat) { dateformat = dv.current().dateformat; }
// info text above table, {0}=duration, {1}=start date, {2}=end date
// parameters can be left out, or the string empty
var infotext = "Upcoming birthdays for {0} from now ({1} – {2})<br><br>";
//=============
function nextBirthday(birthday) {
// Get person’s next birthday on or after "start"
// returns a moment
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
var bdayNext = moment(bday).year(start.year());
if (bdayNext.isBefore(start, 'day')) {
bdayNext.add(1, "year");
}
return bdayNext;
}
function turns(birthday) {
// Get the age in years a person will turn to on their next birthday
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
return nextBirthday(birthday).diff(bday, 'years');
}
function showBirthday(birthday) {
// Determine if this birthday is in the range to be shown
// including the start date, excluding the end date
// because that comes from a duration calculation
// for use with "where", returns true or false
if (birthday) {
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
var bdayNext = nextBirthday(birthday);
if (bdayNext.isBetween(start, end, 'day', '[)')) {
return true;
} else {
return false;
}
} else {
return false;
}
}
function sortByNextBirthday(a, b) {
// comparator function for "sort"
if (nextBirthday(a).isBefore(nextBirthday(b))) {
return -1;
}
if (nextBirthday(a).isAfter(nextBirthday(b))) {
return 1;
}
// they’re equal
return 0;
}
//======================================================================
dv.paragraph(infotext.format(moment.duration(dv.current().duration.toString()).humanize(), start.format(dateformat), end.format(dateformat)));
dv.table(
["Name", "Birthday", "Turns"],
dv.pages(dv.current().searchterm)
// use a function to see if this birthday is in range to be shown
.where(p => showBirthday(p.birthday))
// use a comparator function to sort by next birthday
.sort(p => p.birthday, 'asc', sortByNextBirthday)
.map(p => [
p.file.link,
p.birthday ? nextBirthday(p.birthday).format(dateformat) : '–',
turns(p.birthday)
])
);
```
thanks for your work which works fine on my desktop device. Unfortunately it’s not working on my mobile (iOS) device. I added the same script folder as on my desktop but I get the following error:
I found the solution. By default Obsidian does not sync all file types within your Vault. You need to explicitly allow the synchronization of unknown files in the sync settings on all devices. After a restart of Obsidian everything works fine.
Maybe you could add this as hint to the customJS plugin description.
Hey, folks. I’m sure I’m missing something obvious: I’m working with Metadata Menu and in a lookup field, I want to query only the files that link to the page the field is on. How would I limit them in dv.pages() ? Thank you!
Does anyone have an example of creating a normal HTML table with dataviewjs? Preferably not using literal HTML tag strings? I’m having trouble getting createElement working?