```` ```dataviewjs const container = dv.container; container.style.display = "flex"; container.style.flexDirection = "column"; container.style.gap = "10px"; container.style.marginBottom = "20px"; const dropdownContainer = document.createElement('div'); dropdownContainer.style.display = "flex"; dropdownContainer.style.flexWrap = "wrap"; dropdownContainer.style.gap = "15px"; container.appendChild(dropdownContainer); const controlContainer = document.createElement('div'); controlContainer.style.display = "flex"; controlContainer.style.flexDirection = "column"; controlContainer.style.gap = "10px"; container.appendChild(controlContainer); const separator = document.createElement("div"); separator.innerHTML = "<hr>"; container.appendChild(separator); // Create a small spacer div with two blank lines const filterSpacer = document.createElement("div"); filterSpacer.innerHTML = "<br><br>"; // Insert the spacer before we append the filtersLine container.appendChild(filterSpacer); const filtersLine = document.createElement("div"); filtersLine.style.fontWeight = "bold"; filtersLine.style.marginBottom = "10px"; container.appendChild(filtersLine); const marker = document.createElement("div"); marker.id = "table-marker"; container.appendChild(marker); let levelModified = false, schoolModified = false, damageModified = false, attackModified = false; // We'll use this to remember the selected sort option let currentSortOption = "a-z"; function updateFiltersLine() { const levelSelection = Array.from(levelGroup.querySelectorAll('input:checked')).map(cb => cb.value); const schoolSelection = Array.from(schoolGroup.querySelectorAll('input:checked')).map(cb => cb.value); const damageSelection = Array.from(damageTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); const attackSelection = Array.from(attackTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); let filterTexts = []; if (levelModified && levelSelection.length > 0) { if (levelSelection.includes("All")) { filterTexts.push("<b>All Spell Levels</b>"); } else { filterTexts.push("<b>" + levelSelection.join(", ") + "</b>"); } } if (schoolModified && schoolSelection.length > 0) { if (schoolSelection.includes("All")) { filterTexts.push("<b>All Spell Schools</b>"); } else { filterTexts.push("<b>" + schoolSelection.join(", ") + "</b>"); } } if (damageModified && damageSelection.length > 0) { if (damageSelection.includes("All")) { filterTexts.push("<b>All Damage Types</b>"); } else { filterTexts.push("<b>" + damageSelection.join(", ") + "</b>"); } } if (attackModified && attackSelection.length > 0) { if (attackSelection.includes("All")) { filterTexts.push("<b>All Attack Types</b>"); } else { filterTexts.push("<b>" + attackSelection.join(", ") + "</b>"); } } filtersLine.innerHTML = filterTexts.join(", "); } function createDropdownCheckboxGroup(options, groupName, title) { const groupDiv = document.createElement('div'); groupDiv.style.position = "relative"; groupDiv.style.flex = "1"; groupDiv.style.minWidth = "175px"; groupDiv.style.maxWidth = "75%"; const titleLabel = document.createElement('h4'); titleLabel.textContent = title; titleLabel.style.margin = "0 0 5px 0"; titleLabel.style.color = "#fff"; groupDiv.appendChild(titleLabel); const dropdownButton = document.createElement('button'); dropdownButton.innerHTML = `Select ${title}`; dropdownButton.style.padding = "8px"; dropdownButton.style.border = "1px solid #444"; dropdownButton.style.borderRadius = "4px"; dropdownButton.style.cursor = "pointer"; dropdownButton.style.backgroundColor = "#444"; dropdownButton.style.color = "#fff"; dropdownButton.style.width = "100%"; dropdownButton.style.textAlign = "left"; groupDiv.appendChild(dropdownButton); const dropdownContent = document.createElement('div'); dropdownContent.style.display = "none"; dropdownContent.style.position = "absolute"; dropdownContent.style.backgroundColor = "#333"; dropdownContent.style.border = "1px solid #444"; dropdownContent.style.borderRadius = "4px"; dropdownContent.style.padding = "10px"; dropdownContent.style.width = "100%"; dropdownContent.style.zIndex = "9999"; dropdownContent.style.maxHeight = "200px"; dropdownContent.style.overflowY = "auto"; options.forEach(option => { const label = document.createElement('label'); label.style.color = "#fff"; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = option; checkbox.name = groupName; label.appendChild(checkbox); label.appendChild(document.createTextNode(" " + option)); label.style.display = "flex"; label.style.alignItems = "center"; label.style.marginBottom = "5px"; label.style.padding = "5px"; label.style.borderRadius = "3px"; label.style.cursor = "pointer"; label.addEventListener('mouseover', () => label.style.backgroundColor = "#555"); label.addEventListener('mouseout', () => label.style.backgroundColor = "transparent"); dropdownContent.appendChild(label); }); dropdownButton.addEventListener('click', function() { dropdownContent.style.display = (dropdownContent.style.display === "none") ? "block" : "none"; }); document.addEventListener('click', function(event) { if (!groupDiv.contains(event.target)) { dropdownContent.style.display = "none"; } }); groupDiv.appendChild(dropdownContent); return groupDiv; } let levelSet = [...new Set(dv.pages('"Compendium/Spells"').map(p => p.level))].sort((a, b) => a - b); let schoolSet = [...new Set(dv.pages('"Compendium/Spells"').map(p => p.school))].sort(); let damageTypeOptions = ['All'].concat(['Fire', 'Cold', 'Lightning', 'Necrotic', 'Radiant', 'Force', 'Poison', 'Acid', 'Psychic', 'Bludgeoning', 'Piercing', 'Slashing']); let attackTypeOptions = ['All'].concat(['Melee', 'Ranged']); const levelGroup = createDropdownCheckboxGroup(['All'].concat(levelSet.map(l => `Level ${l}`)), 'level', 'Spell Level'); const schoolGroup = createDropdownCheckboxGroup(['All'].concat(schoolSet), 'school', 'Spell School'); const damageTypeGroup = createDropdownCheckboxGroup(damageTypeOptions, 'damageType', 'Damage Type'); const attackTypeGroup = createDropdownCheckboxGroup(attackTypeOptions, 'attackType', 'Attack Type'); dropdownContainer.appendChild(levelGroup); dropdownContainer.appendChild(schoolGroup); dropdownContainer.appendChild(damageTypeGroup); dropdownContainer.appendChild(attackTypeGroup); levelGroup.querySelectorAll('input').forEach(cb => { cb.addEventListener('change', () => { levelModified = true; updateFiltersLine(); }); }); schoolGroup.querySelectorAll('input').forEach(cb => { cb.addEventListener('change', () => { schoolModified = true; updateFiltersLine(); }); }); damageTypeGroup.querySelectorAll('input').forEach(cb => { cb.addEventListener('change', () => { damageModified = true; updateFiltersLine(); }); }); attackTypeGroup.querySelectorAll('input').forEach(cb => { cb.addEventListener('change', () => { attackModified = true; updateFiltersLine(); }); }); const goButton = document.createElement('button'); goButton.innerHTML = "MALOSO VOBISCUM ET CUM SPIRITUM"; goButton.style.marginTop = "10px"; goButton.style.padding = "8px 15px"; goButton.style.border = "none"; goButton.style.backgroundColor = "#444"; goButton.style.color = "#fff"; goButton.style.cursor = "pointer"; goButton.style.borderRadius = "4px"; goButton.addEventListener('mouseover', () => goButton.style.backgroundColor = "#555"); goButton.addEventListener('mouseout', () => goButton.style.backgroundColor = "#444"); controlContainer.appendChild(goButton); const resetButton = document.createElement('button'); resetButton.innerHTML = "Reset Filters"; resetButton.style.marginTop = "10px"; resetButton.style.padding = "8px 15px"; resetButton.style.border = "none"; resetButton.style.backgroundColor = "#444"; resetButton.style.color = "#fff"; resetButton.style.cursor = "pointer"; resetButton.style.borderRadius = "4px"; resetButton.addEventListener('mouseover', () => resetButton.style.backgroundColor = "#555"); resetButton.addEventListener('mouseout', () => resetButton.style.backgroundColor = "#444"); controlContainer.appendChild(resetButton); // Ensure "Reset Filters" starts below the "Go" button const buttonSeparator = document.createElement("hr"); controlContainer.appendChild(goButton); controlContainer.appendChild(buttonSeparator); controlContainer.appendChild(resetButton); // Prevent duplicate "Clear and Reset" buttons let clearButton = null; /** * Build a Dataview query code block based on selected filters */ function buildDataviewQuery(selectedLevels, selectedSchools, selectedDamageTypes, selectedAttackTypes) { let conditions = []; // Helper to parse "Level 0" -> 0 function parseLevel(str) { return parseInt(str.replace("Level ", "").trim(), 10); } // Build conditions based on filters (ignoring "All") if (selectedLevels.length > 0 && !selectedLevels.includes("All")) { const orClauses = selectedLevels.map(l => `level = ${parseLevel(l)}`); conditions.push(`(${orClauses.join(" or ")})`); } if (selectedSchools.length > 0 && !selectedSchools.includes("All")) { const orClauses = selectedSchools.map(sch => `school = "${sch}"`); conditions.push(`(${orClauses.join(" or ")})`); } if (selectedDamageTypes.length > 0 && !selectedDamageTypes.includes("All")) { const orClauses = selectedDamageTypes.map(dt => `damage_type = "${dt}"`); conditions.push(`(${orClauses.join(" or ")})`); } if (selectedAttackTypes.length > 0 && !selectedAttackTypes.includes("All")) { const orClauses = selectedAttackTypes.map(at => `attack_type = "${at}"`); conditions.push(`(${orClauses.join(" or ")})`); } let dash = "—"; let query = "```dataview\n"; query += "table without id " + "link(file.path, file.name) as \"Name\", " + `default(level, "${dash}") as "Level", ` + `default(save, "${dash}") as "Saving Throw", ` + `default(duration, "${dash}") as "Duration", ` + `default(casting_time, "${dash}") as "Casting Time", ` + `default(range, "${dash}") as "Range", ` + `default(damage_type, "${dash}") as "Damage Type", ` + `default(school, "${dash}") as "School", ` + `default(attack_type, "${dash}") as "Attack Type"\n`; query += 'from "Compendium/Spells"\n'; if (conditions.length > 0) { query += "where " + conditions.join(" and ") + "\n"; } query += "sort file.name asc\n"; query += "```\n"; return query; } /** * Copies the built Dataview code to clipboard */ function copyAsDataviewCode(actualTableContainer) { const selectedLevels = Array.from(levelGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedSchools = Array.from(schoolGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedDamageTypes = Array.from(damageTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedAttackTypes = Array.from(attackTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); const codeBlock = buildDataviewQuery(selectedLevels, selectedSchools, selectedDamageTypes, selectedAttackTypes); navigator.clipboard.writeText(codeBlock).then(() => { new Notice("Dataview code copied!"); }).catch(err => { console.error(err); new Notice("Failed to copy Dataview code."); }); } /** * Filters and sorts the spells, then renders the table */ function updateTable() { const selectedLevels = Array.from(levelGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedSchools = Array.from(schoolGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedDamageTypes = Array.from(damageTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); const selectedAttackTypes = Array.from(attackTypeGroup.querySelectorAll('input:checked')).map(cb => cb.value); // Convert dv.pages(...) to a plain array for reliable filter + sort let allSpells = dv.pages('"Compendium/Spells"').array(); // Filter by checkboxes (ignoring "All") let filteredSpells = allSpells.filter(spell => { // Filter by level if (selectedLevels.length > 0 && !selectedLevels.includes("All")) { if (!selectedLevels.includes(`Level ${spell.level}`)) { return false; } } // Filter by school if (selectedSchools.length > 0 && !selectedSchools.includes("All")) { if (!selectedSchools.includes(spell.school)) { return false; } } // Filter by damage type if (selectedDamageTypes.length > 0 && !selectedDamageTypes.includes("All")) { if (!selectedDamageTypes.includes(spell.damage_type)) { return false; } } // Filter by attack type if (selectedAttackTypes.length > 0 && !selectedAttackTypes.includes("All")) { if (!selectedAttackTypes.includes(spell.attack_type)) { return false; } } return true; }); // Remove the old table if it exists let next = marker.nextSibling; while (next) { let toRemove = next; next = next.nextSibling; toRemove.remove(); } // Create a container for the new table & buttons const tableContainer = document.createElement('div'); marker.insertAdjacentElement("afterend", tableContainer); const buttonBar = document.createElement("div"); buttonBar.style.display = "flex"; buttonBar.style.gap = "10px"; buttonBar.style.marginBottom = "10px"; tableContainer.appendChild(buttonBar); // Sort dropdown (left of Copy button) const sortDropdown = document.createElement("select"); sortDropdown.style.padding = "6px 12px"; sortDropdown.style.border = "1px solid #444"; sortDropdown.style.backgroundColor = "#333"; sortDropdown.style.color = "#fff"; sortDropdown.style.cursor = "pointer"; sortDropdown.style.borderRadius = "4px"; const optionAZ = document.createElement("option"); optionAZ.value = "a-z"; optionAZ.text = "a-z"; sortDropdown.appendChild(optionAZ); const optionLevel = document.createElement("option"); optionLevel.value = "level"; optionLevel.text = "by level"; sortDropdown.appendChild(optionLevel); const optionDamage = document.createElement("option"); optionDamage.value = "damage"; optionDamage.text = "alpha by damage type"; sortDropdown.appendChild(optionDamage); const optionSchool = document.createElement("option"); optionSchool.value = "school"; optionSchool.text = "alpha by school"; sortDropdown.appendChild(optionSchool); // Reflect current selection sortDropdown.value = currentSortOption; // Re-sort whenever user changes dropdown sortDropdown.addEventListener("change", () => { currentSortOption = sortDropdown.value; updateTable(); }); buttonBar.appendChild(sortDropdown); // "Copy as Dataview Code" button const dvBtn = document.createElement("button"); dvBtn.textContent = "Copy as Dataview Code"; dvBtn.style.padding = "6px 12px"; dvBtn.style.border = "1px solid #444"; dvBtn.style.backgroundColor = "#333"; dvBtn.style.color = "#fff"; dvBtn.style.cursor = "pointer"; dvBtn.style.borderRadius = "4px"; dvBtn.addEventListener("mouseover", () => dvBtn.style.backgroundColor = "#444"); dvBtn.addEventListener("mouseout", () => dvBtn.style.backgroundColor = "#333"); buttonBar.appendChild(dvBtn); // Perform the sort according to currentSortOption if (currentSortOption === "a-z") { // Sort by file name filteredSpells.sort((a, b) => (a.file.name || "").toLowerCase().localeCompare((b.file.name || "").toLowerCase()) ); } else if (currentSortOption === "level") { // Sort by level numerically filteredSpells.sort((a, b) => (a.level ?? 0) - (b.level ?? 0)); } else if (currentSortOption === "damage") { // Sort by damage_type alphabetically filteredSpells.sort((a, b) => (a.damage_type || "").toLowerCase().localeCompare((b.damage_type || "").toLowerCase()) ); } else if (currentSortOption === "school") { // Sort by school alphabetically filteredSpells.sort((a, b) => (a.school || "").toLowerCase().localeCompare((b.school || "").toLowerCase()) ); } // Finally, render the table const actualTableContainer = document.createElement("div"); tableContainer.appendChild(actualTableContainer); if (filteredSpells.length === 0) { dv.paragraph("No spells match your filter criteria.", actualTableContainer); } else { dv.table( [ "Name", "Action", "Level", "Saving Throw", "Duration", "Casting Time", "Range", "Damage Type", "School", "Attack Type" ], filteredSpells.map(spell => [ spell.file.link, `<button style="padding: 4px 8px; border: 1px solid #444; background-color: #333; color: #fff; cursor: pointer; border-radius: 4px;">Add to Spellbook</button>`, spell.level, spell.save || "—", spell.duration || "—", spell.casting_time || "—", spell.range || "—", spell.damage_type || "—", spell.school || "—", spell.attack_type || "—" ]), actualTableContainer ); } // Handle clicks on the "Copy as Dataview Code" button dvBtn.addEventListener("click", () => { copyAsDataviewCode(actualTableContainer); }); } /** * "Go" button logic */ goButton.addEventListener('click', function () { updateTable(); // Ensure "Clear and Reset" button only appears once if (!clearButton) { clearButton = document.createElement('button'); clearButton.innerHTML = "Clear and Reset"; clearButton.style.marginTop = "10px"; clearButton.style.padding = "8px 15px"; clearButton.style.border = "none"; clearButton.style.backgroundColor = "#d9534f"; clearButton.style.color = "#fff"; clearButton.style.cursor = "pointer"; clearButton.style.borderRadius = "4px"; clearButton.addEventListener('mouseover', () => clearButton.style.backgroundColor = "#c9302c"); clearButton.addEventListener('mouseout', () => clearButton.style.backgroundColor = "#d9534f"); clearButton.addEventListener("click", function () { // Clear all checkboxes levelGroup.querySelectorAll('input').forEach(cb => cb.checked = false); schoolGroup.querySelectorAll('input').forEach(cb => cb.checked = false); damageTypeGroup.querySelectorAll('input').forEach(cb => cb.checked = false); attackTypeGroup.querySelectorAll('input').forEach(cb => cb.checked = false); // Reset tracking variables levelModified = false; schoolModified = false; damageModified = false; attackModified = false; // Clear filters display updateFiltersLine(); // Remove the generated table let next = marker.nextSibling; while (next) { let toRemove = next; next = next.nextSibling; toRemove.remove(); } // Remove the "Clear and Reset" button itself clearButton.remove(); clearButton = null; }); // Insert "Clear and Reset" below "Reset Filters" controlContainer.appendChild(clearButton); } }); /** * "Reset Filters" button logic */ resetButton.addEventListener('click', function() { levelGroup.querySelectorAll('input').forEach(cb => cb.checked = false); schoolGroup.querySelectorAll('input').forEach(cb => cb.checked = false); damageTypeGroup.querySelectorAll('input').forEach(cb => cb.checked = false); attackTypeGroup.querySelectorAll('input').forEach(cb => cb.checked = false); levelModified = false; schoolModified = false; damageModified = false; attackModified = false; updateFiltersLine(); }); ```