var DROPDOWN_HTML = ``; var DROPDOWN_ICON_SVG = ` `; /** Main initialization function for Datasette Table interactions */ const initDatasetteTable = function (manager) { // Feature detection if (!window.URLSearchParams) { return; } function getParams() { return new URLSearchParams(location.search); } function paramsToUrl(params) { var s = params.toString(); return s ? "?" + s : location.pathname; } function sortDescUrl(column) { var params = getParams(); params.set("_sort_desc", column); params.delete("_sort"); params.delete("_next"); return paramsToUrl(params); } function sortAscUrl(column) { var params = getParams(); params.set("_sort", column); params.delete("_sort_desc"); params.delete("_next"); return paramsToUrl(params); } function facetUrl(column) { var params = getParams(); params.append("_facet", column); return paramsToUrl(params); } function hideColumnUrl(column) { var params = getParams(); params.append("_nocol", column); return paramsToUrl(params); } function showAllColumnsUrl() { var params = getParams(); params.delete("_nocol"); params.delete("_col"); return paramsToUrl(params); } function notBlankUrl(column) { var params = getParams(); params.set(`${column}__notblank`, "1"); return paramsToUrl(params); } function closeMenu() { menu.style.display = "none"; menu.classList.remove("anim-scale-in"); } const tableWrapper = document.querySelector(manager.selectors.tableWrapper); if (tableWrapper) { tableWrapper.addEventListener("scroll", closeMenu); } document.body.addEventListener("click", (ev) => { /* was this click outside the menu? */ var target = ev.target; while (target && target != menu) { target = target.parentNode; } if (!target) { closeMenu(); } }); function onTableHeaderClick(ev) { ev.preventDefault(); ev.stopPropagation(); menu.innerHTML = DROPDOWN_HTML; var th = ev.target; while (th.nodeName != "TH") { th = th.parentNode; } var rect = th.getBoundingClientRect(); var menuTop = rect.bottom + window.scrollY; var menuLeft = rect.left + window.scrollX; var column = th.getAttribute("data-column"); var params = getParams(); var sort = menu.querySelector("a.dropdown-sort-asc"); var sortDesc = menu.querySelector("a.dropdown-sort-desc"); var facetItem = menu.querySelector("a.dropdown-facet"); var notBlank = menu.querySelector("a.dropdown-not-blank"); var hideColumn = menu.querySelector("a.dropdown-hide-column"); var showAllColumns = menu.querySelector("a.dropdown-show-all-columns"); if (params.get("_sort") == column) { sort.parentNode.style.display = "none"; } else { sort.parentNode.style.display = "block"; sort.setAttribute("href", sortAscUrl(column)); } if (params.get("_sort_desc") == column) { sortDesc.parentNode.style.display = "none"; } else { sortDesc.parentNode.style.display = "block"; sortDesc.setAttribute("href", sortDescUrl(column)); } /* Show hide columns options */ if (params.get("_nocol") || params.get("_col")) { showAllColumns.parentNode.style.display = "block"; showAllColumns.setAttribute("href", showAllColumnsUrl()); } else { showAllColumns.parentNode.style.display = "none"; } if (th.getAttribute("data-is-pk") != "1") { hideColumn.parentNode.style.display = "block"; hideColumn.setAttribute("href", hideColumnUrl(column)); } else { hideColumn.parentNode.style.display = "none"; } /* Only show "Facet by this" if it's not the first column, not selected, not a single PK and the Datasette allow_facet setting is True */ var displayedFacets = Array.from( document.querySelectorAll(".facet-info"), ).map((el) => el.dataset.column); var isFirstColumn = th.parentElement.querySelector("th:first-of-type") == th; var isSinglePk = th.getAttribute("data-is-pk") == "1" && document.querySelectorAll('th[data-is-pk="1"]').length == 1; if ( !DATASETTE_ALLOW_FACET || isFirstColumn || displayedFacets.includes(column) || isSinglePk ) { facetItem.parentNode.style.display = "none"; } else { facetItem.parentNode.style.display = "block"; facetItem.setAttribute("href", facetUrl(column)); } /* Show notBlank option if not selected AND at least one visible blank value */ var tdsForThisColumn = Array.from( th.closest("table").querySelectorAll("td." + th.className), ); if ( params.get(`${column}__notblank`) != "1" && tdsForThisColumn.filter((el) => el.innerText.trim() == "").length ) { notBlank.parentNode.style.display = "block"; notBlank.setAttribute("href", notBlankUrl(column)); } else { notBlank.parentNode.style.display = "none"; } var columnTypeP = menu.querySelector(".dropdown-column-type"); var columnType = th.dataset.columnType; var notNull = th.dataset.columnNotNull == 1 ? " NOT NULL" : ""; if (columnType) { columnTypeP.style.display = "block"; columnTypeP.innerText = `Type: ${columnType.toUpperCase()}${notNull}`; } else { columnTypeP.style.display = "none"; } var columnDescriptionP = menu.querySelector(".dropdown-column-description"); if (th.dataset.columnDescription) { columnDescriptionP.innerText = th.dataset.columnDescription; columnDescriptionP.style.display = "block"; } else { columnDescriptionP.style.display = "none"; } menu.style.position = "absolute"; menu.style.top = menuTop + 6 + "px"; menu.style.left = menuLeft + "px"; menu.style.display = "block"; menu.classList.add("anim-scale-in"); // Custom menu items on each render // Plugin hook: allow adding JS-based additional menu items const columnActionsPayload = { columnName: th.dataset.column, columnNotNull: th.dataset.columnNotNull === "1", columnType: th.dataset.columnType, isPk: th.dataset.isPk === "1", }; const columnItemConfigs = manager.makeColumnActions(columnActionsPayload); const menuList = menu.querySelector("ul"); columnItemConfigs.forEach((itemConfig) => { // Remove items from previous render. We assume entries have unique labels. const existingItems = menuList.querySelectorAll(`li`); Array.from(existingItems) .filter((item) => item.innerText === itemConfig.label) .forEach((node) => { node.remove(); }); const newLink = document.createElement("a"); newLink.textContent = itemConfig.label; newLink.href = itemConfig.href ?? "#"; if (itemConfig.onClick) { newLink.onclick = itemConfig.onClick; } // Attach new elements to DOM const menuItem = document.createElement("li"); menuItem.appendChild(newLink); menuList.appendChild(menuItem); }); // Measure width of menu and adjust position if too far right const menuWidth = menu.offsetWidth; const windowWidth = window.innerWidth; if (menuLeft + menuWidth > windowWidth) { menu.style.left = windowWidth - menuWidth - 20 + "px"; } // Align menu .hook arrow with the column cog icon const hook = menu.querySelector(".hook"); const icon = th.querySelector(".dropdown-menu-icon"); const iconRect = icon.getBoundingClientRect(); const hookLeft = iconRect.left - menuLeft + 1 + "px"; hook.style.left = hookLeft; // Move the whole menu right if the hook is too far right const menuRect = menu.getBoundingClientRect(); if (iconRect.right > menuRect.right) { menu.style.left = iconRect.right - menuWidth + "px"; // And move hook tip as well hook.style.left = menuWidth - 13 + "px"; } } var svg = document.createElement("div"); svg.innerHTML = DROPDOWN_ICON_SVG; svg = svg.querySelector("*"); svg.classList.add("dropdown-menu-icon"); var menu = document.createElement("div"); menu.innerHTML = DROPDOWN_HTML; menu = menu.querySelector("*"); menu.style.position = "absolute"; menu.style.display = "none"; document.body.appendChild(menu); var ths = Array.from( document.querySelectorAll(manager.selectors.tableHeaders), ); ths.forEach((th) => { if (!th.querySelector("a")) { return; } var icon = svg.cloneNode(true); icon.addEventListener("click", onTableHeaderClick); th.appendChild(icon); }); }; /* Add x buttons to the filter rows */ function addButtonsToFilterRows(manager) { var x = "✖"; var rows = Array.from( document.querySelectorAll(manager.selectors.filterRow), ).filter((el) => el.querySelector(".filter-op")); rows.forEach((row) => { var a = document.createElement("a"); a.setAttribute("href", "#"); a.setAttribute("aria-label", "Remove this filter"); a.style.textDecoration = "none"; a.innerText = x; a.addEventListener("click", (ev) => { ev.preventDefault(); let row = ev.target.closest("div"); row.querySelector("select").value = ""; row.querySelector(".filter-op select").value = "exact"; row.querySelector("input.filter-value").value = ""; ev.target.closest("a").style.display = "none"; }); row.appendChild(a); var column = row.querySelector("select"); if (!column.value) { a.style.display = "none"; } }); } /* Set up datalist autocomplete for filter values */ function initAutocompleteForFilterValues(manager) { function createDataLists() { var facetResults = document.querySelectorAll( manager.selectors.facetResults, ); Array.from(facetResults).forEach(function (facetResult) { // Use link text from all links in the facet result var links = Array.from( facetResult.querySelectorAll("li:not(.facet-truncated) a"), ); // Create a datalist element var datalist = document.createElement("datalist"); datalist.id = "datalist-" + facetResult.dataset.column; // Create an option element for each link text links.forEach(function (link) { var option = document.createElement("option"); option.label = link.innerText; option.value = link.dataset.facetValue; datalist.appendChild(option); }); // Add the datalist to the facet result facetResult.appendChild(datalist); }); } createDataLists(); // When any select with name=_filter_column changes, update the datalist document.body.addEventListener("change", function (event) { if (event.target.name === "_filter_column") { event.target .closest(manager.selectors.filterRow) .querySelector(".filter-value") .setAttribute("list", "datalist-" + event.target.value); } }); } // Ensures Table UI is initialized only after the Manager is ready. document.addEventListener("datasette_init", function (evt) { const { detail: manager } = evt; // Main table initDatasetteTable(manager); // Other UI functions with interactive JS needs addButtonsToFilterRows(manager); initAutocompleteForFilterValues(manager); });