Advertisement
N1ito

DIY Dynamic Views - Example (Obsidian)

May 18th, 2025
139
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 27.94 KB | Source Code | 0 0
  1. ```dataviewjs
  2. const mb = app.plugins.getPlugin('obsidian-meta-bind-plugin')?.api;
  3. const filePath = dv.current().file.path;
  4. const bindTargets = {};
  5. const NULL_LABEL = "<none>";
  6. const logicDefaultValue = "OR";
  7. const propertyReferenceNote = "9 Databases/Minecraft Mods/Just Enough Items (JEI) (MC Mod).md";//TODO: get the note automatically by property type?
  8.  
  9.  
  10. this.container.appendChild(createHeader("Filter"));
  11.  
  12. const filterRow1 = document.createElement("div");
  13. filterRow1.className = "mb-flex-row";
  14.  
  15. const searchList = document.createElement("div");
  16. searchList.appendChild(createInlineTextField("Search", "search", this));
  17. searchList.appendChild(createInlineListSuggester("Titles", "titles", this));
  18. filterRow1.appendChild(searchList);
  19.  
  20. const categoryList = document.createElement("div");
  21. categoryList.appendChild(createInlineListSuggester("Categories", "categories", this, true));
  22. categoryList.appendChild(createInlineListSuggester("Forge Categories", "forgeCategories", this, true));
  23. categoryList.appendChild(createInlineListSuggester("Modrinth Categories", "modrinthCategories", this, true));
  24. filterRow1.appendChild(categoryList);
  25.  
  26. this.container.appendChild(filterRow1);
  27.  
  28.  
  29. //this.container.appendChild(document.createElement("hr"));
  30.  
  31.  
  32. const filterRow2 = document.createElement("div");
  33. filterRow2.className = "mb-flex-row";
  34.  
  35. const sortList2 = document.createElement("div");
  36. sortList2.appendChild(createInlineListSuggester("Mod Loaders", "modLoaders", this, true));
  37. sortList2.appendChild(createInlineListSuggester("Game Versions", "gameVersions", this, true));
  38. sortList2.appendChild(createInlineListSuggester("Game Versions Confirmed", "gameVersionsConfirmed", this, true));
  39. sortList2.appendChild(createInlineListSuggester("Mod Packs", "modPacks", this, true));
  40. filterRow2.appendChild(sortList2);
  41.  
  42. const sortList3 = document.createElement("div");
  43. sortList3.appendChild(createBoolInlineSelect("Has Alternative", "hasAlternative", this));
  44. sortList3.appendChild(createBoolInlineSelect("Has Add-On", "hasAddOn", this));
  45. sortList3.appendChild(createInlineListSuggester("Client Side", "clientSide", this, true));
  46. sortList3.appendChild(createInlineListSuggester("Server Side", "serverSide", this, true));
  47. sortList3.appendChild(createBoolInlineSelect("Has Notes", "hasContent", this));
  48. filterRow2.appendChild(sortList3);
  49.  
  50. this.container.appendChild(filterRow2);
  51.  
  52.  
  53.  
  54. this.container.appendChild(createHeader("View"));
  55.  
  56. const viewRow1 = document.createElement("div");
  57. viewRow1.className = "mb-flex-row";
  58.  
  59. const columnsList = document.createElement("div");
  60. columnsList.className = "mb-horizontal-row";
  61. columnsList.appendChild(createInlineSuggesterFromPropertiesInNote("Show Columns", "propertiesToShow", this, propertyReferenceNote, ["titles", "image", "description", "downloadsFormatted"], this));
  62. columnsList.appendChild(createInlineNumberField("Line Width", "lineWidth", this, 1000));
  63. columnsList.appendChild(createInlineNumberField("Image Size", "imageSize", this, 150));
  64. viewRow1.appendChild(columnsList);
  65.  
  66. const groupList = document.createElement("div");
  67. groupList.className = "mb-horizontal-row";
  68. groupList.appendChild(createInlineSelectFromExistingPropertiesInNote("Group By", "groupByProperty", this, propertyReferenceNote));
  69. groupList.appendChild(createInlineSelectFromOptions("Group Sort", "groupSort", ["Alphabetical", "Group Size", "Numeric"], this, "Alphabetical"));
  70. groupList.appendChild(createInlineSelectFromOptions("Group Sort Order", "groupSortOrder", ["asc", "desc"], this, "asc"));
  71. groupList.appendChild(createInlineSelectFromExistingPropertiesInNote("Order By", "orderByProperty", this, propertyReferenceNote, "downloads"));
  72. groupList.appendChild(createInlineSelectFromOptions("Order", "order", ["asc", "desc"], this, "desc"));
  73. viewRow1.appendChild(groupList);
  74.  
  75. this.container.appendChild(viewRow1);
  76.  
  77.  
  78. this.container.appendChild(createHeader("Actions"));
  79.  
  80. const resetAllButton = dv.el("button", "πŸ”„ Reset Defaults", { attr: { class: "mb-button-inner" } });
  81.  
  82. resetAllButton.addEventListener("click", () => {
  83. for (const [_, { bindTarget, defaultValue }] of Object.entries(bindTargets)) {
  84. mb.updateMetadata(bindTarget, () => defaultValue ?? []);
  85. }
  86. });
  87.  
  88.  
  89. this.container.appendChild(resetAllButton);
  90.  
  91. var presetButton1 = dv.el("button", "Modloader Comparison", { attr: { class: "mb-button-inner" } });
  92. presetButton1.addEventListener("click", () => {
  93. setMetaData("propertiesToShow", ["titles", "image", "modLoaders", "alternatives"]);
  94. setMetaData("groupByProperty", "modLoaders");
  95. setMetaData("orderByProperty", "alternatives");
  96. });
  97. this.container.appendChild(presetButton1);
  98.  
  99. var presetButton2 = dv.el("button", "Category Groups", { attr: { class: "mb-button-inner" } });
  100. presetButton2.addEventListener("click", () => {
  101. setMetaData("propertiesToShow", ["titles", "image", "description"]);
  102. setMetaData("groupByProperty", "categories");
  103. });
  104. this.container.appendChild(presetButton2);
  105.  
  106. this.container.appendChild(document.createElement("br"));
  107. this.container.appendChild(document.createElement("br"));
  108.  
  109. const outputContainer = document.createElement("div");
  110. outputContainer.id = "dv-output-container";
  111. this.container.appendChild(outputContainer);
  112.  
  113. var button = dv.el("button", "πŸ” Search", { attr: { id: "dv-search-button", class: "mb-button-inner mod-cta" } });
  114. button.addEventListener("click", renderTable);
  115.  
  116.  
  117. function renderTable()
  118. {
  119. const containerBlock = dv.container.closest(".block-language-dataviewjs");
  120. let clear = false;
  121. for (let child of Array.from(containerBlock.children)) {
  122. if (clear)
  123. child.remove();
  124. if (child.id === "dv-search-button")
  125. clear = true;
  126. }
  127.  
  128. const lineWidth = getMetaData("lineWidth");
  129. if (lineWidth && !isNaN(lineWidth)) {
  130. const scopedElements = document.querySelectorAll(".dynamicQueries");
  131. scopedElements.forEach(scopedElement => scopedElement.style.setProperty('--file-line-width', `${lineWidth}px`, "important"));
  132. //scopedElement.offsetHeight;//triggers reflow
  133. }
  134.  
  135. let search = getMetaData("search");
  136. if(search != null && typeof search === "string")
  137. search = search.toLowerCase();
  138. const titles = getMetaData("titles");
  139. const { values: categories, logic: categoriesLogic } = getFilterData("categories");
  140. const { values: forgeCategories, logic: forgeCategoriesLogic } = getFilterData("forgeCategories");
  141. const { values: modrinthCategories, logic: modrinthCategoriesLogic } = getFilterData("modrinthCategories");
  142.  
  143. //TODO: refactor into filter class
  144. const { values: modLoaders, logic: modLoadersLogic } = getFilterData("modLoaders");
  145. const { values: gameVersions, logic: gameVersionsLogic } = getFilterData("gameVersions");
  146. const { values: gameVersionsConfirmed, logic: gameVersionsConfirmedLogic } = getFilterData("gameVersionsConfirmed");
  147. const { values: modPacks, logic: modPacksLogic } = getFilterData("modPacks");
  148.  
  149. const hasAlternative = getMetaData("hasAlternative");
  150. const hasAddOn = getMetaData("hasAddOn");
  151. const { values: clientSide, logic: clientSideLogic } = getFilterData("clientSide");
  152. const { values: serverSide, logic: serverSideLogic } = getFilterData("serverSide");
  153. const hasContent = getMetaData("hasContent");
  154.  
  155. const orderByProperty = getMetaData("orderByProperty");
  156. const order = getMetaData("order");
  157.  
  158. const propertiesToShow = getMetaData("propertiesToShow");
  159.  
  160. const imageSize = getMetaData("imageSize") ?? "150";
  161.  
  162. const groupSort = getMetaData("groupSort") ?? "Alphabetical";
  163. const groupSortOrder = getMetaData("groupSortOrder") ?? "asc";
  164.  
  165. let results = dv.pages().where(p => {
  166. if (!p.type || p.type !== "Mod" || p.file.path.includes("Templates"))
  167. return false;
  168.  
  169. if (!matchesMultiValueField(p.titles, titles))
  170. return false;
  171.  
  172. if (!matchesMultiValueField(p.categories, categories, categoriesLogic))
  173. return false;
  174.  
  175. if (!matchesMultiValueField(p.forgeCategories, forgeCategories, forgeCategoriesLogic))
  176. return false;
  177.  
  178. if (!matchesMultiValueField(p.modrinthCategories, modrinthCategories, modrinthCategoriesLogic))
  179. return false;
  180.  
  181. if (!matchesMultiValueField(p.modLoaders, modLoaders, modLoadersLogic))
  182. return false;
  183.  
  184. if (!matchesMultiValueField(p.gameVersions, gameVersions, gameVersionsLogic))
  185. return false;
  186.  
  187. if (!matchesMultiValueField(p.gameVersionsConfirmed, gameVersionsConfirmed, gameVersionsConfirmedLogic))
  188. return false;
  189.  
  190.  
  191. if (!matchesLinkArrayField(p.modPacks, modPacks, modPacksLogic))
  192. return false;
  193.  
  194. if (hasAlternative != null)
  195. {
  196. if (hasAlternative == "yes" && (!p.alternatives || !Array.isArray(p.alternatives) || p.alternatives.length == 0))
  197. return false;
  198. if (hasAlternative == "no" && (p.alternatives && Array.isArray(p.alternatives) && p.alternatives.length != 0))
  199. return false;
  200. }
  201.  
  202. if (hasAddOn != null)
  203. {
  204. if (hasAddOn == "yes" && (!p["add-ons"] || !Array.isArray(p["add-ons"]) || p["add-ons"].length == 0))
  205. return false;
  206. if (hasAddOn == "no" && (p["add-ons"] && Array.isArray(p["add-ons"]) && p["add-ons"].length != 0))
  207. return false;
  208. }
  209.  
  210. //if (clientSide != null && clientSide != "" && p.clientSide != clientSide)// Could be used for bools?
  211. // return false;
  212.  
  213. //if (clientSide && clientSide.length > 0 && (!p.clientSide || !clientSide.some(c => c == p.clientSide)))
  214. // return false;
  215. //if (serverSide && serverSide.length > 0 && (!p.serverSide || !serverSide.some(c => c == p.serverSide)))
  216. // return false;
  217.  
  218. if (!matchesSingleValueField(p.clientSide, clientSide, clientSideLogic))
  219. return false;
  220. if (!matchesSingleValueField(p.serverSide, serverSide, serverSideLogic))
  221. return false;
  222.  
  223. if (hasContent != null)
  224. {
  225. if (hasContent === "yes" && (!p.file?.lists || p.file.lists.filter(item => item.text && item.text.trim() !== "").length === 0))
  226. return false;
  227. if (hasContent === "no" && p.file?.lists && p.file.lists.some(item => item.text && item.text.trim() !== ""))
  228. return false;
  229. }
  230.  
  231. if (!isUnset(search)) {
  232. const nameMatch = matchesCaseInsensitive(p.file.name, search);
  233. const aliasMatch = matchesAnyInCollection(search, p.file.aliases);
  234. //const content = await dv.io.load(p.file.path);//TODO: include non-bullet point content
  235. const contentMatch = matchesAnyInCollection(search, p.file.lists.map(l => l.text));
  236. //const metadataMatch = matchesAnyInCollection(search, Object.entries(p.file.frontmatter));
  237.  
  238. if (!nameMatch && !aliasMatch && !contentMatch)// && !metadataMatch)
  239. return false;
  240. }
  241. return true;
  242. });
  243.  
  244. function matchesAnyInCollection(search, collection) {
  245. if(!collection)
  246. return false;
  247. return collection.filter(v => typeof v === "string" || Array.isArray(v))
  248. .some(v => {
  249. if (Array.isArray(v)) {
  250. return v.some(item => typeof item === "string" && matchesCaseInsensitive(item,search));
  251. }
  252. return typeof v === "string" && matchesCaseInsensitive(v, search);
  253. });
  254. }
  255.  
  256. function matchesCaseInsensitive(text, searchText) {
  257. return text.toLowerCase().contains(searchText.toLowerCase());
  258. }
  259.  
  260.  
  261. let orderKey = orderByProperty;
  262. if (Array.isArray(orderKey))
  263. orderKey = orderKey[0];
  264. if (orderKey && typeof orderKey === "string") {
  265. let orderByName = orderKey == "name";
  266. results = results
  267. .where(p => orderByName ? true : p[orderKey] !== undefined)
  268. .sort(p => orderByName ? p.file.name : p[orderKey], order);
  269. }
  270.  
  271.  
  272. const headers = [...propertiesToShow];
  273.  
  274. const rows = results.map(p => {
  275. const row = [];
  276.  
  277. for (let key of propertiesToShow) {
  278. let val = p[key];
  279.  
  280. if (key === "image") {
  281. if (Array.isArray(val)) val = val[0];
  282. val = val ? `![img|${imageSize}](${val.replace("/default.jpg", "/maxresdefault.jpg")})` : 'No image';
  283. val = val.replace(/\|/g, "\\|");
  284. } else if (key === "name") {
  285. val = markdownLink(p.file.path, p.file.name);
  286. } else if (key === "titles") {
  287. const title = val && val.length > 0 ? val[0].replace(/\(YT\)$/, '').trim() : 'Untitled';
  288. val = markdownLink(p.file.path, title);
  289. } else if (key === "content") {
  290. const contentList = p.file?.lists?.filter(item => item.text && item.text.trim() !== "").map(item => item.text);
  291. val = contentList?.length > 0 ? contentList.join("<br>") : "";
  292. } else {
  293. if (Array.isArray(val)) {
  294. val = `<ul>${val.map(item => {
  295. if (typeof item === "object" && item.path)
  296. return `<li>[[${item.path}]]</li>`;
  297. return `<li>${item}</li>`;
  298. }).join("")}</ul>`;
  299. }
  300. else if (typeof val === "object" && val?.path) val = `[[${val.path}]]`;
  301. else if (val === true) val = "βœ…";
  302. else if (val === false) val = "❌";
  303. else val = val ?? "";
  304. }
  305.  
  306. row.push(val);
  307. }
  308.  
  309. return row;
  310. });
  311.  
  312. //needed for callouts to work
  313. function markdownLink(filePath, displayName) {
  314. const encodedPath = encodeURI(filePath.replace(".md", ""));
  315. const safeDisplay = displayName.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
  316. return `[${safeDisplay}](${encodedPath})`;
  317. }
  318.  
  319. const calloutMap = {
  320. /*"admin": "abstract",
  321. "performance": "abstract",
  322. "magic": "tip",
  323. "combat": "danger",
  324. "mobs": "bug",
  325. "chat": "quote",
  326. "farming": "info",
  327. "utility": "note",
  328. "decoration": "quote",
  329. "skins": "note",
  330. "roleplay": "note",
  331. "_uncategorized": "warning"*/
  332. };
  333.  
  334. //TODO: expose as field?
  335. function getCategoryEmoji(category) {
  336. const map = {
  337. "decorations": "πŸͺ‘",
  338. "visuals": "🎨",
  339. "exploration": "🧭",
  340. "technology": "πŸ› οΈ",
  341. "map": "πŸ—ΊοΈ",
  342. "structures": "🏠",
  343. "transportation": "πŸš‹",//πŸ›»
  344. "skins": "🧍",
  345. "roleplay": "πŸ“–",
  346. "realism": "πŸ’§",
  347. "quality-of-life": "πŸ‘",
  348. "performance": "⚑",
  349. "NPC": "πŸ€–",
  350. "mobs": "πŸ‘Ή",
  351. "magic": "πŸͺ„",//πŸ§™
  352. "items": "πŸ’",
  353. "cosmetics": "πŸ‘”",
  354. "combat": "βš”οΈ",
  355. "chat": "πŸ—―οΈ",
  356. "camera": "πŸ“·",
  357. "audio": "πŸ”Š",
  358. "animations": "πŸƒ",
  359. "admin": "βš™οΈ",
  360. "balancing": "βš–οΈ",
  361. "dimension": "πŸšͺ",
  362. "classes": "🧝",
  363. "nuilding": "πŸ—οΈ",
  364. "library": "πŸ“š",
  365. };
  366. return map[category] ?? "γ…€";
  367. }
  368.  
  369. const groupByProperty = getMetaData("groupByProperty");
  370. const groupKey = Array.isArray(groupByProperty) ? groupByProperty[0] : groupByProperty;
  371.  
  372. if (groupKey) {
  373. const grouped = {};
  374.  
  375. for (let i = 0; i < results.length; i++) {
  376. const page = results[i];
  377. let keys = page[groupKey];
  378.  
  379. if (!keys || keys.length === 0)
  380. keys = ["_uncategorized"];
  381. else if (!Array.isArray(keys))
  382. keys = [keys];
  383.  
  384. for (const k of keys) {
  385. const keyString = typeof k === "object" && k?.path ? k.path : String(k);
  386. if (!grouped[keyString]) grouped[keyString] = [];
  387. grouped[keyString].push({ page, row: rows[i] });
  388. }
  389. }
  390.  
  391. let sortedKeys = Object.keys(grouped);
  392.  
  393. if (groupSort === "Alphabetical") {
  394. sortedKeys.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  395. } else if (groupSort === "Group Size") {
  396. sortedKeys.sort((a, b) => grouped[a].length - grouped[b].length);
  397. } else if (groupSort === "Numeric") {
  398. const parseVersion = (str) => {
  399. const ver = String(str).match(/(\d+(\.\d+)*)/);
  400. const match = ver ? ver[0].split(".").map(n => parseInt(n)) : [0];
  401. return match.concat(Array(3 - match.length).fill(0)); // pad to [x, y, z]
  402. };
  403.  
  404. sortedKeys.sort((a, b) => {
  405. const [a1, a2, a3] = parseVersion(a);
  406. const [b1, b2, b3] = parseVersion(b);
  407.  
  408. if (a1 !== b1) return a1 - b1;
  409. if (a2 !== b2) return a2 - b2;
  410. return a3 - b3;
  411. });
  412. }
  413.  
  414. if (groupSortOrder === "desc")
  415. sortedKeys.reverse();
  416.  
  417. for (const key of sortedKeys) {
  418. const calloutType = calloutMap[key.toLowerCase?.()] ?? "empty";
  419. const emoji = getCategoryEmoji?.(key) ?? ""; // fallback if key isn't a category
  420.  
  421. const tableHeader = `| ${headers.join(" | ")} |\n| ${headers.map(() => "---").join(" | ")} |`;
  422. const tableRows = grouped[key].map(obj => `| ${obj.row.join(" | ")} |`);
  423.  
  424. const callout = [
  425. `> [!${calloutType}]- ${emoji} ${key} (${grouped[key].length})`,
  426. `>`,
  427. ...tableHeader.split("\n").map(line => `> ${line}`),
  428. ...tableRows.map(row => `> ${row}`),
  429. `>`
  430. ].join("\n");
  431.  
  432. dv.paragraph(callout);
  433. }
  434. } else {
  435. dv.table(headers, rows);
  436. }
  437. }
  438.  
  439.  
  440. function getFilterData(propertyName) {
  441. const values = getMetaData(propertyName);
  442. const logic = (getMetaData(`${propertyName}_logic`) ?? logicDefaultValue).toUpperCase();
  443. return { values, logic };
  444. }
  445.  
  446. function setMetaData(name, value)
  447. {
  448. mb.setMetadata(mb.createBindTarget('memory', filePath, [name], false), value);
  449. }
  450. function getMetaData(name)
  451. {
  452. return mb.getMetadata(bindTargets[name]?.bindTarget);
  453. //mb.getMetadata(mb.createBindTarget('memory', filePath, [name], false));
  454. }
  455.  
  456. function setUpBindTarget(name, defaultValue)
  457. {
  458. const bindTarget = mb.createBindTarget('memory', filePath, [name], false);
  459. if(mb.getMetadata(bindTarget) == undefined)
  460. mb.setMetadata(bindTarget, defaultValue);
  461. return bindTarget;
  462. }
  463.  
  464.  
  465. function matchesFilter(propertyValue, selectedValues, logic = logicDefaultValue) {
  466. if (!selectedValues || selectedValues.length === 0)
  467. return true;
  468.  
  469. if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
  470. return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);// We expect this field to be null or an empty array
  471.  
  472. if (!propertyValue)
  473. return false;
  474.  
  475. if (Array.isArray(propertyValue)) {
  476. const values = propertyValue.map(val => val?.path ?? val?.toString?.() ?? val);
  477. if (logic === "AND") {
  478. return selectedValues.every(sel => values.includes(sel));
  479. } else if (logic === "OR") {
  480. return selectedValues.some(sel => values.includes(sel));
  481. }
  482. } else {
  483. const val = propertyValue?.path ?? propertyValue?.toString?.() ?? propertyValue;
  484. return selectedValues.includes(val);
  485. }
  486.  
  487. return false;
  488. }
  489.  
  490. function matchesMultiValueField(propertyValue, selectedValues, logic = logicDefaultValue) {
  491. if (!selectedValues || selectedValues.length === 0)
  492. return true;
  493.  
  494. if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
  495. return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);
  496.  
  497. if (!Array.isArray(propertyValue))
  498. {
  499. if (logic === "NOT")
  500. return selectedValues != propertyValue;
  501. return selectedValues == propertyValue;
  502. }
  503.  
  504. if (logic === "AND")
  505. return selectedValues.every(val => propertyValue.includes(val));
  506. else if (logic === "OR")
  507. return propertyValue.some(val => selectedValues.includes(val));
  508. else if (logic === "NOT")
  509. return !propertyValue.some(val => selectedValues.includes(val));
  510.  
  511. throw new Error("Unsupported filter logic.");
  512. }
  513.  
  514. function matchesLinkArrayField(propertyValue, selectedValues, logic = logicDefaultValue) {
  515. if (!selectedValues || selectedValues.length === 0)
  516. return true;
  517.  
  518. if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
  519. return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);
  520.  
  521. if (!Array.isArray(propertyValue))
  522. return false;
  523.  
  524. const paths = propertyValue.map(val => val?.path ?? val?.toString?.());
  525. if (logic === "AND") {
  526. return selectedValues.every(sel => paths.includes(sel));
  527. } else if (logic === "OR") {
  528. return selectedValues.some(sel => paths.includes(sel));
  529. } else if (logic === "NOT") {
  530. return !selectedValues.some(sel => paths.includes(sel));
  531. }
  532.  
  533. throw new Error("Unsupported filter logic.");
  534. }
  535.  
  536. function matchesSingleValueField(propertyValue, selectedValues, logic = logicDefaultValue) {
  537. if (!selectedValues || selectedValues.length === 0)
  538. return true;
  539.  
  540. if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
  541. return propertyValue === null || propertyValue === undefined || propertyValue === "";
  542.  
  543. const val = propertyValue?.path ?? propertyValue?.toString?.() ?? propertyValue;
  544.  
  545. if (logic === "AND") {
  546. // For single values, "AND" means the value must match *all* selected (only possible if there's one selected)
  547. return selectedValues.length === 1 && selectedValues[0] === val;
  548. } else if (logic === "OR") {
  549. return selectedValues.includes(val);
  550. } else if (logic === "NOT") {
  551. return !selectedValues.includes(val);
  552. }
  553. throw new Error("Unsupported filter logic.");
  554. }
  555.  
  556.  
  557.  
  558. function createHeader(name) {
  559. const header = document.createElement("h3");
  560. header.textContent = name;
  561. return header;
  562. }
  563.  
  564. function createInlineSelectFromOptions(labelText, propertyName, optionsArray, note, defaultValue = null) {
  565. const filePath = dv.current().file.path;
  566.  
  567. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  568.  
  569. const selectArgs = optionsArray.map(option => ({
  570. name: 'option',
  571. value: [option],
  572. ...(defaultValue === option ? { isSelected: true } : {})
  573. }));
  574.  
  575. selectArgs.push({
  576. name: 'title',
  577. value: ['Choose an option'],
  578. });
  579.  
  580. const field = mb.createInputFieldMountable(filePath, {
  581. renderChildType: 'inline',
  582. declaration: {
  583. inputFieldType: 'inlineSelect',
  584. bindTarget: bindTarget,
  585. arguments: selectArgs,
  586. },
  587. });
  588.  
  589. return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
  590. }
  591.  
  592. function createInlineSelectFromExistingPropertiesInNote(labelText, propertyName, note, targetNotePath, defaultValue = null) {
  593. const filePath = dv.current().file.path;
  594.  
  595. const frontmatter = app.metadataCache.getCache(targetNotePath)?.frontmatter || {};
  596.  
  597. const keys = Object.keys(frontmatter).filter(k => !k.startsWith("cssclass"));
  598. keys.push("name");
  599.  
  600. const selectArgs = keys.map(key => ({
  601. name: 'option',
  602. value: [key],
  603. ...(defaultValue === key ? { isSelected: true } : {})
  604. }));
  605.  
  606. selectArgs.push({
  607. name: 'title',
  608. value: ['Choose a property'],
  609. });
  610.  
  611. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  612.  
  613. const field = mb.createInputFieldMountable(filePath, {
  614. renderChildType: 'inline',
  615. declaration: {
  616. inputFieldType: 'inlineSelect',
  617. bindTarget: bindTarget,
  618. arguments: selectArgs,
  619. },
  620. });
  621.  
  622. return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
  623. }
  624.  
  625. function createInlineSuggesterFromPropertiesInNote(labelText, propertyName, note, targetNotePath, defaultValue = []) {
  626. const filePath = dv.current().file.path;
  627.  
  628. const frontmatter = app.metadataCache.getCache(targetNotePath)?.frontmatter || {};
  629.  
  630. let keys = Object.keys(frontmatter).filter(k => !k.startsWith("cssclass")); // Optional filtering
  631.  
  632. const extraKeys = ["content"];
  633. keys = [...new Set([...keys, ...extraKeys])];
  634.  
  635. const suggesterArgs = keys.map(key => ({
  636. name: 'option',
  637. value: [key],
  638. }));
  639.  
  640. suggesterArgs.push({
  641. name: 'title',
  642. value: ['Choose a property'],
  643. });
  644.  
  645. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  646.  
  647. const field = mb.createInputFieldMountable(filePath, {
  648. renderChildType: 'inline',
  649. declaration: {
  650. inputFieldType: 'inlineListSuggester',
  651. bindTarget: bindTarget,
  652. arguments: suggesterArgs,
  653. },
  654. });
  655.  
  656. return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
  657. }
  658.  
  659.  
  660. function createInlineListSuggester(labelText, propertyName, note, logicToggle = false, defaultValue = null) {
  661. const filePath = dv.current().file.path;
  662.  
  663. const pages = dv.pages().where(p => p[propertyName]);
  664. let values = [];
  665.  
  666. for (let p of pages) {
  667. const val = p[propertyName];
  668. if (Array.isArray(val)) {
  669. val.forEach(v => values.push(normalizeValue(v)));
  670. } else {
  671. values.push(normalizeValue(val));
  672. }
  673. }
  674. values.push(NULL_LABEL);
  675.  
  676. function normalizeValue(v) {
  677. if (typeof v === "string") return v;
  678. if (typeof v === "object" && v.path) return v.path; // Likely a Link object from [[wikilinks]]
  679. return String(v); // Fallback
  680. }
  681.  
  682. const uniqueValues = Array.from(new Set(values));
  683.  
  684. const suggesterArgs = uniqueValues.map(val => ({
  685. name: 'option',
  686. value: [val],
  687. }));
  688.  
  689. suggesterArgs.push({
  690. name: 'title',
  691. value: ['Choose a status'],
  692. });
  693.  
  694. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  695.  
  696. const field = mb.createInputFieldMountable(filePath, {
  697. renderChildType: 'inline',
  698. declaration: {
  699. inputFieldType: 'inlineListSuggester',
  700. bindTarget: bindTarget,
  701. arguments: suggesterArgs,
  702. },
  703. });
  704.  
  705. return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue, logicToggle);
  706. }
  707.  
  708. function createBoolInlineSelect(labelText, propertyName, note) {
  709. const filePath = dv.current().file.path;
  710.  
  711. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  712.  
  713. const selectArgs = [
  714. { name: 'option', value: ['yes'] },
  715. { name: 'option', value: ['no'] },
  716. { name: 'option', value: ['-'] },
  717. { name: 'title', value: ['Choose an option'] },
  718. ];
  719.  
  720. const field = mb.createInputFieldMountable(filePath, {
  721. renderChildType: 'inline',
  722. declaration: {
  723. inputFieldType: 'inlineSelect',
  724. bindTarget: bindTarget,
  725. arguments: selectArgs,
  726. },
  727. });
  728.  
  729. return buildListItem(labelText, field, bindTarget, note, propertyName);
  730. }
  731.  
  732. function createInlineTextField(labelText, propertyName, note, defaultValue = null) {
  733. return createInlineTextOrNumberField(false, labelText, propertyName, note, defaultValue);
  734. }
  735.  
  736. function createInlineNumberField(labelText, propertyName, note, defaultValue = null) {
  737. return createInlineTextOrNumberField(true, labelText, propertyName, note, defaultValue);
  738. }
  739.  
  740. function createInlineTextOrNumberField(onlyNumbers, labelText, propertyName, note, defaultValue = null) {
  741. const filePath = dv.current().file.path;
  742.  
  743. const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
  744.  
  745. const field = mb.createInputFieldMountable(filePath, {
  746. renderChildType: 'inline',
  747. declaration: {
  748. inputFieldType: onlyNumbers ? 'number' : 'text',
  749. bindTarget: bindTarget,
  750. },
  751. });
  752.  
  753. return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
  754. }
  755.  
  756. function buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue = null, logicToggle = false) {
  757. if (propertyName)
  758. bindTargets[propertyName] = { bindTarget, defaultValue };
  759.  
  760. const container = document.createElement('div');
  761. container.className = "mb-field-item";
  762. const label = document.createElement('label');
  763. label.textContent = labelText + ": ";
  764. label.style.marginRight = "0.5em";
  765. container.appendChild(label);
  766.  
  767. const fieldContainer = document.createElement('span');
  768. mb.wrapInMDRC(field, fieldContainer, note.component);
  769. container.appendChild(fieldContainer);
  770.  
  771. const filePath = dv.current().file.path;
  772. const fileCache = app.metadataCache.getCache(filePath);
  773. const valueKey = propertyName ?? bindTarget?.key ?? "";
  774. const currentValue = mb.getMetadata(bindTarget);
  775.  
  776. if (isUnset(currentValue) && defaultValue !== null)
  777. mb.updateMetadata(bindTarget, () => defaultValue);
  778.  
  779. let logicBind;
  780. if (logicToggle && propertyName) {
  781. const logicProp = `${propertyName}_logic`;
  782. logicBind = setUpBindTarget(logicProp, logicDefaultValue);
  783. bindTargets[logicProp] = { bindTarget: logicBind, defaultValue: logicDefaultValue };
  784.  
  785. const toggle = mb.createInputFieldMountable(filePath, {
  786. renderChildType: 'inline',
  787. declaration: {
  788. inputFieldType: 'inlineSelect',
  789. bindTarget: logicBind,
  790. arguments: [
  791. { name: 'option', value: ['AND'] },
  792. { name: 'option', value: ['OR'], isSelected: true },
  793. { name: 'option', value: ['NOT'] },
  794. { name: 'title', value: ['Match Logic'] }
  795. ]
  796. }
  797. });
  798.  
  799. const toggleContainer = document.createElement("span");
  800. toggleContainer.style.marginLeft = "0.5em";
  801. mb.wrapInMDRC(toggle, toggleContainer, note.component);
  802. container.appendChild(toggleContainer);
  803. }
  804.  
  805. const resetButtonEl = document.createElement('button');
  806. resetButtonEl.textContent = 'Reset';
  807. resetButtonEl.style.marginLeft = '0.5em';
  808. resetButtonEl.onclick = () => {
  809. mb.updateMetadata(bindTarget, () => defaultValue);
  810. if(logicBind)
  811. mb.updateMetadata(logicBind, () => logicDefaultValue);
  812. };
  813. container.appendChild(resetButtonEl);
  814.  
  815. return container;
  816. }
  817.  
  818. function isUnset(value) {
  819. return value === undefined || value === null || (Array.isArray(value) && value.length === 0);
  820. }
  821. ```
  822.  
Tags: Obsidian
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement