Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ```dataviewjs
- const mb = app.plugins.getPlugin('obsidian-meta-bind-plugin')?.api;
- const filePath = dv.current().file.path;
- const bindTargets = {};
- const NULL_LABEL = "<none>";
- const logicDefaultValue = "OR";
- const propertyReferenceNote = "exampleDatabase/Mod Entry 1.md"; // <- fill this with a path to your reference note
- this.container.appendChild(createHeader("Filter"));
- const filterRow1 = document.createElement("div");
- filterRow1.className = "mb-flex-row";
- const searchList = document.createElement("div");
- searchList.appendChild(createInlineTextField("Search", "search", this));
- searchList.appendChild(createInlineListSuggester("Categories", "categories", this, true));
- filterRow1.appendChild(searchList);
- this.container.appendChild(filterRow1);
- this.container.appendChild(createHeader("View"));
- const viewRow1 = document.createElement("div");
- viewRow1.className = "mb-flex-row";
- const columnsList = document.createElement("div");
- columnsList.className = "mb-horizontal-row";
- columnsList.appendChild(createInlineSuggesterFromPropertiesInNote("Show Columns", "propertiesToShow", this, propertyReferenceNote, ["name", "image", "content"], this));
- columnsList.appendChild(createInlineNumberField("Line Width", "lineWidth", this, 1000));
- columnsList.appendChild(createInlineNumberField("Image Size", "imageSize", this, 150));
- viewRow1.appendChild(columnsList);
- const groupList = document.createElement("div");
- groupList.className = "mb-horizontal-row";
- groupList.appendChild(createInlineSelectFromExistingPropertiesInNote("Group By", "groupByProperty", this, propertyReferenceNote));
- groupList.appendChild(createInlineSelectFromOptions("Group Sort", "groupSort", ["Alphabetical", "Group Size", "Numeric"], this, "Alphabetical"));
- groupList.appendChild(createInlineSelectFromOptions("Group Sort Order", "groupSortOrder", ["asc", "desc"], this, "asc"));
- groupList.appendChild(createInlineSelectFromExistingPropertiesInNote("Order By", "orderByProperty", this, propertyReferenceNote, "name"));
- groupList.appendChild(createInlineSelectFromOptions("Order", "order", ["asc", "desc"], this, "asc"));
- viewRow1.appendChild(groupList);
- this.container.appendChild(viewRow1);
- this.container.appendChild(createHeader("Actions"));
- const resetAllButton = dv.el("button", "π Reset Defaults", { attr: { class: "mb-button-inner" } });
- resetAllButton.addEventListener("click", () => {
- for (const [_, { bindTarget, defaultValue }] of Object.entries(bindTargets)) {
- mb.updateMetadata(bindTarget, () => defaultValue ?? []);
- }
- });
- this.container.appendChild(resetAllButton);
- this.container.appendChild(document.createElement("br"));
- this.container.appendChild(document.createElement("br"));
- const outputContainer = document.createElement("div");
- outputContainer.id = "dv-output-container";
- this.container.appendChild(outputContainer);
- var button = dv.el("button", "π Search", { attr: { id: "dv-search-button", class: "mb-button-inner mod-cta" } });
- button.addEventListener("click", renderTable);
- function renderTable()
- {
- const containerBlock = dv.container.closest(".block-language-dataviewjs");
- let clear = false;
- for (let child of Array.from(containerBlock.children)) {
- if (clear)
- child.remove();
- if (child.id === "dv-search-button")
- clear = true;
- }
- const lineWidth = getMetaData("lineWidth");
- if (lineWidth && !isNaN(lineWidth)) {
- const scopedElements = document.querySelectorAll(".dynamicQueries");
- scopedElements.forEach(scopedElement => scopedElement.style.setProperty('--file-line-width', `${lineWidth}px`, "important"));
- //scopedElement.offsetHeight;//triggers reflow
- }
- let search = getMetaData("search");
- if(search != null && typeof search === "string")
- search = search.toLowerCase();
- const { values: categories, logic: categoriesLogic } = getFilterData("categories");
- //TODO: refactor into filter class
- const orderByProperty = getMetaData("orderByProperty");
- const order = getMetaData("order");
- const propertiesToShow = getMetaData("propertiesToShow");
- const imageSize = getMetaData("imageSize") ?? "150";
- const groupSort = getMetaData("groupSort") ?? "Alphabetical";
- const groupSortOrder = getMetaData("groupSortOrder") ?? "asc";
- let results = dv.pages().where(p => {
- if (!p.type || p.type !== "Mod" || p.file.path.includes("Templates"))
- return false;
- if (!matchesMultiValueField(p.categories, categories, categoriesLogic))
- return false;
- if (!isUnset(search)) {
- const nameMatch = matchesCaseInsensitive(p.file.name, search);
- const aliasMatch = matchesAnyInCollection(search, p.file.aliases);
- //const content = await dv.io.load(p.file.path);//TODO: include non-bullet point content
- const contentMatch = matchesAnyInCollection(search, p.file.lists.map(l => l.text));
- //const metadataMatch = matchesAnyInCollection(search, Object.entries(p.file.frontmatter));
- if (!nameMatch && !aliasMatch && !contentMatch)// && !metadataMatch)
- return false;
- }
- return true;
- });
- function matchesAnyInCollection(search, collection) {
- if(!collection)
- return false;
- return collection.filter(v => typeof v === "string" || Array.isArray(v))
- .some(v => {
- if (Array.isArray(v)) {
- return v.some(item => typeof item === "string" && matchesCaseInsensitive(item,search));
- }
- return typeof v === "string" && matchesCaseInsensitive(v, search);
- });
- }
- function matchesCaseInsensitive(text, searchText) {
- return text.toLowerCase().contains(searchText.toLowerCase());
- }
- let orderKey = orderByProperty;
- if (Array.isArray(orderKey))
- orderKey = orderKey[0];
- if (orderKey && typeof orderKey === "string") {
- let orderByName = orderKey == "name";
- results = results
- .where(p => orderByName ? true : p[orderKey] !== undefined)
- .sort(p => orderByName ? p.file.name : p[orderKey], order);
- }
- const headers = [...propertiesToShow];
- const rows = results.map(p => {
- const row = [];
- for (let key of propertiesToShow) {
- let val = p[key];
- if (key === "image") {
- if (Array.isArray(val)) val = val[0];
- val = val ? `})` : 'No image';
- val = val.replace(/\|/g, "\\|");
- } else if (key === "name") {
- val = markdownLink(p.file.path, p.file.name);
- } else if (key === "content") {
- const contentList = p.file?.lists?.filter(item => item.text && item.text.trim() !== "").map(item => item.text);
- val = contentList?.length > 0 ? contentList.join("<br>") : "";
- } else {
- if (Array.isArray(val)) {
- val = `<ul>${val.map(item => {
- if (typeof item === "object" && item.path)
- return `<li>[[${item.path}]]</li>`;
- return `<li>${item}</li>`;
- }).join("")}</ul>`;
- }
- else if (typeof val === "object" && val?.path) val = `[[${val.path}]]`;
- else if (val === true) val = "β ";
- else if (val === false) val = "β";
- else val = val ?? "";
- }
- row.push(val);
- }
- return row;
- });
- //needed for callouts to work
- function markdownLink(filePath, displayName) {
- const encodedPath = encodeURI(filePath.replace(".md", ""));
- const safeDisplay = displayName.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
- return `[${safeDisplay}](${encodedPath})`;
- }
- const calloutMap = {
- /*"admin": "abstract",
- "performance": "abstract",
- "magic": "tip",
- "combat": "danger",
- "mobs": "bug",
- "chat": "quote",
- "farming": "info",
- "utility": "note",
- "decoration": "quote",
- "skins": "note",
- "roleplay": "note",
- "_uncategorized": "warning"*/
- };
- //TODO: expose as field?
- function getCategoryEmoji(category) {
- const map = {
- "decorations": "πͺ",
- "visuals": "π¨",
- "exploration": "π§",
- "technology": "π οΈ",
- "map": "πΊοΈ",
- "structures": "π ",
- "transportation": "π",//π»
- "skins": "π§",
- "roleplay": "π",
- "realism": "π§",
- "quality-of-life": "π",
- "performance": "β‘",
- "NPC": "π€",
- "mobs": "πΉ",
- "magic": "πͺ",//π§
- "items": "π",
- "cosmetics": "π",
- "combat": "βοΈ",
- "chat": "π―οΈ",
- "camera": "π·",
- "audio": "π",
- "animations": "π",
- "admin": "βοΈ",
- "balancing": "βοΈ",
- "dimension": "πͺ",
- "classes": "π§",
- "nuilding": "ποΈ",
- };
- return map[category] ?? "γ €";
- }
- const groupByProperty = getMetaData("groupByProperty");
- const groupKey = Array.isArray(groupByProperty) ? groupByProperty[0] : groupByProperty;
- if (groupKey) {
- const grouped = {};
- for (let i = 0; i < results.length; i++) {
- const page = results[i];
- let keys = page[groupKey];
- if (!keys || keys.length === 0)
- keys = ["_uncategorized"];
- else if (!Array.isArray(keys))
- keys = [keys];
- for (const k of keys) {
- const keyString = typeof k === "object" && k?.path ? k.path : String(k);
- if (!grouped[keyString]) grouped[keyString] = [];
- grouped[keyString].push({ page, row: rows[i] });
- }
- }
- let sortedKeys = Object.keys(grouped);
- if (groupSort === "Alphabetical") {
- sortedKeys.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
- } else if (groupSort === "Group Size") {
- sortedKeys.sort((a, b) => grouped[a].length - grouped[b].length);
- } else if (groupSort === "Numeric") {
- const parseVersion = (str) => {
- const ver = String(str).match(/(\d+(\.\d+)*)/);
- const match = ver ? ver[0].split(".").map(n => parseInt(n)) : [0];
- return match.concat(Array(3 - match.length).fill(0)); // pad to [x, y, z]
- };
- sortedKeys.sort((a, b) => {
- const [a1, a2, a3] = parseVersion(a);
- const [b1, b2, b3] = parseVersion(b);
- if (a1 !== b1) return a1 - b1;
- if (a2 !== b2) return a2 - b2;
- return a3 - b3;
- });
- }
- if (groupSortOrder === "desc")
- sortedKeys.reverse();
- for (const key of sortedKeys) {
- const calloutType = calloutMap[key.toLowerCase?.()] ?? "empty";
- const emoji = getCategoryEmoji?.(key) ?? ""; // fallback if key isn't a category
- const tableHeader = `| ${headers.join(" | ")} |\n| ${headers.map(() => "---").join(" | ")} |`;
- const tableRows = grouped[key].map(obj => `| ${obj.row.join(" | ")} |`);
- const callout = [
- `> [!${calloutType}]- ${emoji} ${key} (${grouped[key].length})`,
- `>`,
- ...tableHeader.split("\n").map(line => `> ${line}`),
- ...tableRows.map(row => `> ${row}`),
- `>`
- ].join("\n");
- dv.paragraph(callout);
- }
- } else {
- dv.table(headers, rows);
- }
- }
- function getFilterData(propertyName) {
- const values = getMetaData(propertyName);
- const logic = (getMetaData(`${propertyName}_logic`) ?? logicDefaultValue).toUpperCase();
- return { values, logic };
- }
- function setMetaData(name, value)
- {
- mb.setMetadata(mb.createBindTarget('memory', filePath, [name], false), value);
- }
- function getMetaData(name)
- {
- return mb.getMetadata(bindTargets[name]?.bindTarget);
- //mb.getMetadata(mb.createBindTarget('memory', filePath, [name], false));
- }
- function setUpBindTarget(name, defaultValue)
- {
- const bindTarget = mb.createBindTarget('memory', filePath, [name], false);
- if(mb.getMetadata(bindTarget) == undefined)
- mb.setMetadata(bindTarget, defaultValue);
- return bindTarget;
- }
- function matchesFilter(propertyValue, selectedValues, logic = logicDefaultValue) {
- if (!selectedValues || selectedValues.length === 0)
- return true;
- if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
- return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);// We expect this field to be null or an empty array
- if (!propertyValue)
- return false;
- if (Array.isArray(propertyValue)) {
- const values = propertyValue.map(val => val?.path ?? val?.toString?.() ?? val);
- if (logic === "AND") {
- return selectedValues.every(sel => values.includes(sel));
- } else if (logic === "OR") {
- return selectedValues.some(sel => values.includes(sel));
- }
- } else {
- const val = propertyValue?.path ?? propertyValue?.toString?.() ?? propertyValue;
- return selectedValues.includes(val);
- }
- return false;
- }
- function matchesMultiValueField(propertyValue, selectedValues, logic = logicDefaultValue) {
- if (!selectedValues || selectedValues.length === 0)
- return true;
- if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
- return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);
- if (!Array.isArray(propertyValue))
- {
- if (logic === "NOT")
- return selectedValues != propertyValue;
- return selectedValues == propertyValue;
- }
- if (logic === "AND")
- return selectedValues.every(val => propertyValue.includes(val));
- else if (logic === "OR")
- return propertyValue.some(val => selectedValues.includes(val));
- else if (logic === "NOT")
- return !propertyValue.some(val => selectedValues.includes(val));
- throw new Error("Unsupported filter logic.");
- }
- function matchesLinkArrayField(propertyValue, selectedValues, logic = logicDefaultValue) {
- if (!selectedValues || selectedValues.length === 0)
- return true;
- if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
- return !propertyValue || (Array.isArray(propertyValue) && propertyValue.length === 0);
- if (!Array.isArray(propertyValue))
- return false;
- const paths = propertyValue.map(val => val?.path ?? val?.toString?.());
- if (logic === "AND") {
- return selectedValues.every(sel => paths.includes(sel));
- } else if (logic === "OR") {
- return selectedValues.some(sel => paths.includes(sel));
- } else if (logic === "NOT") {
- return !selectedValues.some(sel => paths.includes(sel));
- }
- throw new Error("Unsupported filter logic.");
- }
- function matchesSingleValueField(propertyValue, selectedValues, logic = logicDefaultValue) {
- if (!selectedValues || selectedValues.length === 0)
- return true;
- if (selectedValues.length === 1 && selectedValues[0] === NULL_LABEL)
- return propertyValue === null || propertyValue === undefined || propertyValue === "";
- const val = propertyValue?.path ?? propertyValue?.toString?.() ?? propertyValue;
- if (logic === "AND") {
- // For single values, "AND" means the value must match *all* selected (only possible if there's one selected)
- return selectedValues.length === 1 && selectedValues[0] === val;
- } else if (logic === "OR") {
- return selectedValues.includes(val);
- } else if (logic === "NOT") {
- return !selectedValues.includes(val);
- }
- throw new Error("Unsupported filter logic.");
- }
- function createHeader(name) {
- const header = document.createElement("h3");
- header.textContent = name;
- return header;
- }
- function createInlineSelectFromOptions(labelText, propertyName, optionsArray, note, defaultValue = null) {
- const filePath = dv.current().file.path;
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const selectArgs = optionsArray.map(option => ({
- name: 'option',
- value: [option],
- ...(defaultValue === option ? { isSelected: true } : {})
- }));
- selectArgs.push({
- name: 'title',
- value: ['Choose an option'],
- });
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineSelect',
- bindTarget: bindTarget,
- arguments: selectArgs,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
- }
- function createInlineSelectFromExistingPropertiesInNote(labelText, propertyName, note, targetNotePath, defaultValue = null) {
- const filePath = dv.current().file.path;
- const frontmatter = app.metadataCache.getCache(targetNotePath)?.frontmatter || {};
- const keys = Object.keys(frontmatter).filter(k => !k.startsWith("cssclass"));
- keys.push("name");
- const selectArgs = keys.map(key => ({
- name: 'option',
- value: [key],
- ...(defaultValue === key ? { isSelected: true } : {})
- }));
- selectArgs.push({
- name: 'title',
- value: ['Choose a property'],
- });
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineSelect',
- bindTarget: bindTarget,
- arguments: selectArgs,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
- }
- function createInlineSuggesterFromPropertiesInNote(labelText, propertyName, note, targetNotePath, defaultValue = []) {
- const filePath = dv.current().file.path;
- const frontmatter = app.metadataCache.getCache(targetNotePath)?.frontmatter || {};
- let keys = Object.keys(frontmatter).filter(k => !k.startsWith("cssclass")); // Optional filtering
- const extraKeys = ["name", "content"];
- keys = [...new Set([...keys, ...extraKeys])];
- const suggesterArgs = keys.map(key => ({
- name: 'option',
- value: [key],
- }));
- suggesterArgs.push({
- name: 'title',
- value: ['Choose a property'],
- });
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineListSuggester',
- bindTarget: bindTarget,
- arguments: suggesterArgs,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
- }
- function createInlineListSuggester(labelText, propertyName, note, logicToggle = false, defaultValue = null) {
- const filePath = dv.current().file.path;
- const pages = dv.pages().where(p => p[propertyName]);
- let values = [];
- for (let p of pages) {
- const val = p[propertyName];
- if (Array.isArray(val)) {
- val.forEach(v => values.push(normalizeValue(v)));
- } else {
- values.push(normalizeValue(val));
- }
- }
- values.push(NULL_LABEL);
- function normalizeValue(v) {
- if (typeof v === "string") return v;
- if (typeof v === "object" && v.path) return v.path; // Likely a Link object from [[wikilinks]]
- return String(v); // Fallback
- }
- const uniqueValues = Array.from(new Set(values));
- const suggesterArgs = uniqueValues.map(val => ({
- name: 'option',
- value: [val],
- }));
- suggesterArgs.push({
- name: 'title',
- value: ['Choose a status'],
- });
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineListSuggester',
- bindTarget: bindTarget,
- arguments: suggesterArgs,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue, logicToggle);
- }
- function createBoolInlineSelect(labelText, propertyName, note) {
- const filePath = dv.current().file.path;
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const selectArgs = [
- { name: 'option', value: ['yes'] },
- { name: 'option', value: ['no'] },
- { name: 'option', value: ['-'] },
- { name: 'title', value: ['Choose an option'] },
- ];
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineSelect',
- bindTarget: bindTarget,
- arguments: selectArgs,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName);
- }
- function createInlineTextField(labelText, propertyName, note, defaultValue = null) {
- return createInlineTextOrNumberField(false, labelText, propertyName, note, defaultValue);
- }
- function createInlineNumberField(labelText, propertyName, note, defaultValue = null) {
- return createInlineTextOrNumberField(true, labelText, propertyName, note, defaultValue);
- }
- function createInlineTextOrNumberField(onlyNumbers, labelText, propertyName, note, defaultValue = null) {
- const filePath = dv.current().file.path;
- const bindTarget = mb.createBindTarget('memory', filePath, [propertyName], false);
- const field = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: onlyNumbers ? 'number' : 'text',
- bindTarget: bindTarget,
- },
- });
- return buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue);
- }
- function buildListItem(labelText, field, bindTarget, note, propertyName, defaultValue = null, logicToggle = false) {
- if (propertyName)
- bindTargets[propertyName] = { bindTarget, defaultValue };
- const container = document.createElement('div');
- container.className = "mb-field-item";
- const label = document.createElement('label');
- label.textContent = labelText + ": ";
- label.style.marginRight = "0.5em";
- container.appendChild(label);
- const fieldContainer = document.createElement('span');
- mb.wrapInMDRC(field, fieldContainer, note.component);
- container.appendChild(fieldContainer);
- const filePath = dv.current().file.path;
- const fileCache = app.metadataCache.getCache(filePath);
- const valueKey = propertyName ?? bindTarget?.key ?? "";
- const currentValue = mb.getMetadata(bindTarget);
- if (isUnset(currentValue) && defaultValue !== null)
- mb.updateMetadata(bindTarget, () => defaultValue);
- let logicBind;
- if (logicToggle && propertyName) {
- const logicProp = `${propertyName}_logic`;
- logicBind = setUpBindTarget(logicProp, logicDefaultValue);
- bindTargets[logicProp] = { bindTarget: logicBind, defaultValue: logicDefaultValue };
- const toggle = mb.createInputFieldMountable(filePath, {
- renderChildType: 'inline',
- declaration: {
- inputFieldType: 'inlineSelect',
- bindTarget: logicBind,
- arguments: [
- { name: 'option', value: ['AND'] },
- { name: 'option', value: ['OR'], isSelected: true },
- { name: 'option', value: ['NOT'] },
- { name: 'title', value: ['Match Logic'] }
- ]
- }
- });
- const toggleContainer = document.createElement("span");
- toggleContainer.style.marginLeft = "0.5em";
- mb.wrapInMDRC(toggle, toggleContainer, note.component);
- container.appendChild(toggleContainer);
- }
- const resetButtonEl = document.createElement('button');
- resetButtonEl.textContent = 'Reset';
- resetButtonEl.style.marginLeft = '0.5em';
- resetButtonEl.onclick = () => {
- mb.updateMetadata(bindTarget, () => defaultValue);
- if(logicBind)
- mb.updateMetadata(logicBind, () => logicDefaultValue);
- };
- container.appendChild(resetButtonEl);
- return container;
- }
- function isUnset(value) {
- return value === undefined || value === null || (Array.isArray(value) && value.length === 0);
- }
- ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement