diff --git a/datasette/static/app.css b/datasette/static/app.css
index 4183b58e..0a6efd4c 100644
--- a/datasette/static/app.css
+++ b/datasette/static/app.css
@@ -742,6 +742,284 @@ p.zero-results {
.select-wrapper.small-screen-only {
display: none;
}
+
+@keyframes datasette-modal-slide-in {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@keyframes datasette-modal-fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+dialog.mobile-column-actions-dialog {
+ --ink: #0f0f0f;
+ --paper: #f5f3ef;
+ --muted: #6b6b6b;
+ --rule: #e2dfd8;
+ --accent: #1a56db;
+ --card: #ffffff;
+ border: none;
+ border-radius: var(--modal-border-radius, 0.75rem);
+ padding: 0;
+ margin: auto;
+ width: min(420px, calc(100vw - 32px));
+ max-width: 95vw;
+ max-height: min(640px, calc(100vh - 32px));
+ box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04));
+ animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out;
+ overflow: hidden;
+ font-family: system-ui, -apple-system, sans-serif;
+ background: var(--card);
+}
+
+dialog.mobile-column-actions-dialog[open] {
+ display: flex;
+ flex-direction: column;
+}
+
+dialog.mobile-column-actions-dialog::backdrop {
+ background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5));
+ backdrop-filter: var(--modal-backdrop-blur, blur(4px));
+ -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px));
+ animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out;
+}
+
+.mobile-column-actions-dialog .modal-header {
+ padding: 20px 24px 16px;
+ border-bottom: 1px solid var(--rule);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.mobile-column-actions-dialog .modal-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--ink);
+}
+
+.mobile-column-actions-dialog .modal-meta {
+ font-family: ui-monospace, monospace;
+ font-size: 0.7rem;
+ color: var(--muted);
+ background: var(--paper);
+ padding: 3px 9px;
+ border-radius: 20px;
+}
+
+.mobile-column-actions-dialog .list-wrap {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ position: relative;
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+}
+
+.mobile-column-actions-dialog .list-wrap::before,
+.mobile-column-actions-dialog .list-wrap::after {
+ content: "";
+ position: sticky;
+ display: block;
+ left: 0;
+ right: 0;
+ height: 20px;
+ pointer-events: none;
+ z-index: 5;
+}
+
+.mobile-column-actions-dialog .list-wrap::before {
+ top: 0;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.9), transparent);
+}
+
+.mobile-column-actions-dialog .list-wrap::after {
+ bottom: 0;
+ background: linear-gradient(to top, rgba(255,255,255,0.9), transparent);
+ margin-top: -20px;
+}
+
+.mobile-column-top-actions {
+ padding: 10px 24px 0;
+}
+
+.mobile-column-top-action {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.mobile-column-section {
+ border-bottom: 1px solid var(--rule);
+}
+
+.mobile-column-actions-dialog .col-header {
+ width: 100%;
+ padding: 12px 24px;
+ font: inherit;
+ font-weight: 600;
+ border: 0;
+ background: none;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ text-align: left;
+}
+
+.mobile-column-header-text {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.mobile-column-name {
+ color: var(--ink);
+}
+
+.mobile-column-meta {
+ color: var(--muted);
+ font-size: 0.78em;
+ font-family: ui-monospace, monospace;
+ font-weight: normal;
+}
+
+.mobile-column-chevron {
+ color: var(--muted);
+ transition: transform 0.2s ease-out;
+}
+
+.mobile-column-actions-dialog .col-header[aria-expanded="true"] .mobile-column-chevron {
+ transform: rotate(180deg);
+}
+
+.mobile-column-actions-dialog .col-actions[hidden] {
+ display: none;
+}
+
+.mobile-column-actions-dialog .col-actions ul,
+.mobile-column-actions-dialog .col-actions li {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.mobile-column-actions-dialog .col-actions a,
+.mobile-column-actions-dialog .col-actions button {
+ display: block;
+ width: 100%;
+ padding: 10px 24px 10px 40px;
+ color: var(--ink);
+ text-align: left;
+ font: inherit;
+ text-decoration: none;
+ background: none;
+ border: 0;
+ border-top: 1px solid #f5f5f5;
+ cursor: pointer;
+}
+
+.mobile-column-actions-dialog .col-actions a:hover,
+.mobile-column-actions-dialog .col-actions button:hover {
+ background: var(--paper);
+}
+
+.mobile-column-actions-dialog .col-actions a:active,
+.mobile-column-actions-dialog .col-actions button:active {
+ background: #eee;
+}
+
+.mobile-column-description,
+.mobile-column-no-actions {
+ margin: 0;
+ padding: 0 24px 12px 24px;
+ color: var(--muted);
+ font-size: 0.85em;
+}
+
+.mobile-column-actions-dialog .modal-footer {
+ padding: 14px 20px;
+ border-top: 1px solid var(--rule);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+ background: var(--paper);
+}
+
+.mobile-column-actions-dialog .footer-info {
+ flex: 1;
+ font-family: ui-monospace, monospace;
+ font-size: 0.68rem;
+ color: var(--muted);
+}
+
+.mobile-column-actions-dialog .btn {
+ border: none;
+ border-radius: 5px;
+ padding: 9px 20px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ cursor: pointer;
+ touch-action: manipulation;
+ font-family: inherit;
+ transition: background 0.12s;
+}
+
+.mobile-column-actions-dialog .btn-ghost {
+ background: transparent;
+ color: var(--muted);
+ border: 1px solid var(--rule);
+}
+
+.mobile-column-actions-dialog .btn-ghost:hover {
+ background: var(--rule);
+ color: var(--ink);
+}
+
+@media (max-width: 640px) {
+ dialog.mobile-column-actions-dialog {
+ width: 95vw;
+ max-height: 85vh;
+ border-radius: 0.5rem;
+ }
+
+ .mobile-column-actions-dialog .modal-header {
+ padding: 16px 18px 14px;
+ }
+
+ .mobile-column-top-actions {
+ padding-left: 18px;
+ padding-right: 18px;
+ }
+
+ .mobile-column-actions-dialog .col-header {
+ padding-left: 18px;
+ padding-right: 18px;
+ }
+
+ .mobile-column-actions-dialog .col-actions a,
+ .mobile-column-actions-dialog .col-actions button {
+ padding-left: 34px;
+ padding-right: 18px;
+ }
+
+ .mobile-column-description,
+ .mobile-column-no-actions {
+ padding-left: 18px;
+ padding-right: 18px;
+ }
+}
+
@media only screen and (max-width: 576px) {
.small-screen-only {
@@ -803,16 +1081,42 @@ p.zero-results {
.filters input.filter-value {
width: 140px;
}
- button.choose-columns-mobile {
- display: inline-block;
+ button.choose-columns-mobile,
+ button.column-actions-mobile {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
padding: 0.5rem 1rem;
margin-bottom: 1em;
font-size: 0.9rem;
+ line-height: 1.2;
font-family: inherit;
background: white;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
+ vertical-align: top;
+ box-sizing: border-box;
+ min-height: 2.5rem;
+ }
+
+ button.column-actions-mobile {
+ gap: 0.55rem;
+ }
+
+ button.column-actions-mobile svg {
+ display: block;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ }
+
+ button.column-actions-mobile span {
+ line-height: 1.2;
+ }
+
+ button.choose-columns-mobile {
+ margin-right: 0.5rem;
}
}
diff --git a/datasette/static/mobile-column-actions.js b/datasette/static/mobile-column-actions.js
new file mode 100644
index 00000000..0f407cd2
--- /dev/null
+++ b/datasette/static/mobile-column-actions.js
@@ -0,0 +1,318 @@
+var MOBILE_COLUMN_BREAKPOINT = 576;
+var MOBILE_COLUMN_DIALOG_ID = "mobile-column-actions-dialog";
+var MOBILE_COLUMN_DIALOG_TITLE_ID = "mobile-column-actions-title";
+
+function mobileColumnHeaders(manager) {
+ return Array.from(
+ document.querySelectorAll(manager.selectors.tableHeaders),
+ ).filter((th) => th.dataset.column);
+}
+
+function mobileColumnMetaText(th) {
+ var parts = [];
+ if (th.dataset.columnType) {
+ parts.push(th.dataset.columnType);
+ }
+ if (th.dataset.isPk === "1") {
+ parts.push("pk");
+ }
+ if (th.dataset.columnNotNull === "1") {
+ parts.push("not null");
+ }
+ return parts.join(", ");
+}
+
+function createMobileColumnActionNode(itemConfig, closeDialog) {
+ var actionNode;
+ if (itemConfig.href) {
+ actionNode = document.createElement("a");
+ actionNode.href = itemConfig.href;
+ } else {
+ actionNode = document.createElement("button");
+ actionNode.type = "button";
+ }
+ actionNode.textContent = itemConfig.label;
+
+ if (itemConfig.onClick) {
+ actionNode.addEventListener("click", function (ev) {
+ try {
+ itemConfig.onClick.call(actionNode, ev);
+ } finally {
+ closeDialog({ restoreFocus: false });
+ }
+ });
+ }
+
+ return actionNode;
+}
+
+function initMobileColumnActions(manager) {
+ var triggerButton = document.querySelector(".column-actions-mobile");
+ if (!triggerButton) {
+ return;
+ }
+
+ if (
+ !window.URLSearchParams ||
+ !window.HTMLDialogElement ||
+ !manager.columnActions
+ ) {
+ triggerButton.style.display = "none";
+ return;
+ }
+
+ if (!mobileColumnHeaders(manager).length) {
+ triggerButton.style.display = "none";
+ return;
+ }
+
+ var dialog = document.createElement("dialog");
+ dialog.className = "mobile-column-actions-dialog";
+ dialog.id = MOBILE_COLUMN_DIALOG_ID;
+ dialog.setAttribute("aria-labelledby", MOBILE_COLUMN_DIALOG_TITLE_ID);
+ dialog.innerHTML = `
+
+
+
+ `;
+ document.body.appendChild(dialog);
+
+ triggerButton.setAttribute("aria-haspopup", "dialog");
+ triggerButton.setAttribute("aria-controls", MOBILE_COLUMN_DIALOG_ID);
+ triggerButton.setAttribute("aria-expanded", "false");
+
+ var countEl = dialog.querySelector(".modal-meta");
+ var listWrap = dialog.querySelector(".mobile-column-list");
+ var doneButton = dialog.querySelector(".mobile-column-actions-done");
+ var expandedSectionId = null;
+ var shouldRestoreFocus = true;
+
+ function updateExpandedSection() {
+ Array.from(dialog.querySelectorAll(".col-header")).forEach((button) => {
+ var controlsId = button.getAttribute("aria-controls");
+ var actionList = dialog.querySelector("#" + controlsId);
+ var isExpanded = controlsId === expandedSectionId;
+ button.setAttribute("aria-expanded", isExpanded ? "true" : "false");
+ actionList.hidden = !isExpanded;
+ actionList.classList.toggle("expanded", isExpanded);
+ });
+ }
+
+ function scrollExpandedSectionIntoView(section) {
+ var sectionTop = section.offsetTop;
+ var sectionBottom = sectionTop + section.offsetHeight;
+ var visibleTop = listWrap.scrollTop;
+ var visibleBottom = visibleTop + listWrap.clientHeight;
+ var sectionHeight = section.offsetHeight;
+
+ if (sectionTop < visibleTop) {
+ listWrap.scrollTop = sectionTop;
+ return;
+ }
+
+ if (sectionBottom <= visibleBottom) {
+ return;
+ }
+
+ if (sectionHeight <= listWrap.clientHeight) {
+ listWrap.scrollTop = sectionBottom - listWrap.clientHeight;
+ } else {
+ listWrap.scrollTop = sectionTop;
+ }
+ }
+
+ function closeDialog(options) {
+ options = options || {};
+ shouldRestoreFocus = options.restoreFocus !== false;
+ if (dialog.open) {
+ dialog.close();
+ } else {
+ triggerButton.setAttribute("aria-expanded", "false");
+ if (shouldRestoreFocus) {
+ triggerButton.focus();
+ }
+ }
+ }
+
+ function renderDialog() {
+ var headers = mobileColumnHeaders(manager);
+ if (!headers.length) {
+ closeDialog({ restoreFocus: false });
+ triggerButton.style.display = "none";
+ return false;
+ }
+
+ if (
+ !headers.some(
+ (_th, index) => `mobile-column-actions-${index}` === expandedSectionId,
+ )
+ ) {
+ expandedSectionId = null;
+ }
+
+ countEl.textContent = `${headers.length} column${
+ headers.length === 1 ? "" : "s"
+ }`;
+ listWrap.innerHTML = "";
+
+ if (manager.columnActions.shouldShowShowAllColumns()) {
+ var topActions = document.createElement("div");
+ topActions.className = "mobile-column-top-actions";
+
+ var showAllColumns = document.createElement("a");
+ showAllColumns.className = "btn btn-ghost mobile-column-top-action";
+ showAllColumns.href = manager.columnActions.showAllColumnsUrl();
+ showAllColumns.textContent = "Show all columns";
+
+ topActions.appendChild(showAllColumns);
+ listWrap.appendChild(topActions);
+ }
+
+ headers.forEach((th, index) => {
+ var sectionId = `mobile-column-actions-${index}`;
+ var actionState = manager.columnActions.buildColumnActionState(th, {
+ includeChooseColumns: false,
+ includeShowAllColumns: false,
+ });
+ var section = document.createElement("section");
+ section.className = "mobile-column-section";
+
+ var headerButton = document.createElement("button");
+ headerButton.type = "button";
+ headerButton.className = "col-header";
+ headerButton.setAttribute("aria-controls", sectionId);
+ headerButton.setAttribute("aria-expanded", "false");
+
+ var headerText = document.createElement("span");
+ headerText.className = "mobile-column-header-text";
+
+ var name = document.createElement("span");
+ name.className = "mobile-column-name";
+ name.textContent = th.dataset.column;
+ headerText.appendChild(name);
+
+ var metaText = mobileColumnMetaText(th);
+ if (metaText) {
+ var meta = document.createElement("span");
+ meta.className = "mobile-column-meta";
+ meta.textContent = metaText;
+ headerText.appendChild(meta);
+ }
+
+ var chevron = document.createElement("span");
+ chevron.className = "mobile-column-chevron";
+ chevron.setAttribute("aria-hidden", "true");
+ chevron.textContent = "▾";
+
+ headerButton.appendChild(headerText);
+ headerButton.appendChild(chevron);
+ headerButton.addEventListener("click", function () {
+ expandedSectionId = expandedSectionId === sectionId ? null : sectionId;
+ updateExpandedSection();
+ if (expandedSectionId === sectionId) {
+ scrollExpandedSectionIntoView(section);
+ }
+ });
+
+ var actionContainer = document.createElement("div");
+ actionContainer.id = sectionId;
+ actionContainer.className = "col-actions";
+ actionContainer.hidden = true;
+
+ if (actionState.columnDescription) {
+ var description = document.createElement("p");
+ description.className = "mobile-column-description";
+ description.textContent = actionState.columnDescription;
+ actionContainer.appendChild(description);
+ }
+
+ if (actionState.actionItems.length) {
+ var actionList = document.createElement("ul");
+ actionState.actionItems.forEach((itemConfig) => {
+ var actionItem = document.createElement("li");
+ actionItem.appendChild(
+ createMobileColumnActionNode(itemConfig, closeDialog),
+ );
+ actionList.appendChild(actionItem);
+ });
+ actionContainer.appendChild(actionList);
+ } else {
+ var noActions = document.createElement("p");
+ noActions.className = "mobile-column-no-actions";
+ noActions.textContent = "No actions available";
+ actionContainer.appendChild(noActions);
+ }
+
+ section.appendChild(headerButton);
+ section.appendChild(actionContainer);
+ listWrap.appendChild(section);
+ });
+
+ updateExpandedSection();
+ return true;
+ }
+
+ function openDialog() {
+ if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT) {
+ return;
+ }
+ if (!renderDialog()) {
+ return;
+ }
+ if (!dialog.open) {
+ dialog.showModal();
+ }
+ triggerButton.setAttribute("aria-expanded", "true");
+ var focusTarget =
+ dialog.querySelector(".mobile-column-top-action") ||
+ dialog.querySelector(".col-header") ||
+ doneButton;
+ focusTarget.focus();
+ }
+
+ triggerButton.addEventListener("click", function () {
+ if (dialog.open) {
+ closeDialog();
+ } else {
+ openDialog();
+ }
+ });
+
+ doneButton.addEventListener("click", function () {
+ closeDialog();
+ });
+
+ dialog.addEventListener("click", function (ev) {
+ if (ev.target === dialog) {
+ closeDialog();
+ }
+ });
+
+ dialog.addEventListener("cancel", function (ev) {
+ ev.preventDefault();
+ closeDialog();
+ });
+
+ dialog.addEventListener("close", function () {
+ triggerButton.setAttribute("aria-expanded", "false");
+ if (shouldRestoreFocus) {
+ triggerButton.focus();
+ }
+ });
+
+ window.addEventListener("resize", function () {
+ if (window.innerWidth > MOBILE_COLUMN_BREAKPOINT && dialog.open) {
+ closeDialog({ restoreFocus: false });
+ }
+ });
+}
+
+document.addEventListener("datasette_init", function (evt) {
+ initMobileColumnActions(evt.detail);
+});
diff --git a/datasette/static/table.js b/datasette/static/table.js
index c26dda5a..707bfe86 100644
--- a/datasette/static/table.js
+++ b/datasette/static/table.js
@@ -1,14 +1,6 @@
var DROPDOWN_HTML = ``;
@@ -18,54 +10,230 @@ var DROPDOWN_ICON_SVG = ``;
+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 getDisplayedFacets() {
+ return Array.from(document.querySelectorAll(".facet-info")).map(
+ (el) => el.dataset.column,
+ );
+}
+
+function getColumnClassName(th) {
+ return Array.from(th.classList).find((className) =>
+ className.startsWith("col-"),
+ );
+}
+
+function getColumnCells(th) {
+ var table = th.closest("table");
+ var columnClassName = getColumnClassName(th);
+ if (!table || !columnClassName) {
+ return [];
+ }
+ return Array.from(table.querySelectorAll("td." + columnClassName));
+}
+
+function getColumnMeta(th) {
+ return {
+ columnName: th.dataset.column,
+ columnNotNull: th.dataset.columnNotNull === "1",
+ columnType: th.dataset.columnType,
+ isPk: th.dataset.isPk === "1",
+ };
+}
+
+function getColumnTypeText(th) {
+ var columnType = th.dataset.columnType;
+ if (!columnType) {
+ return null;
+ }
+ var notNull = th.dataset.columnNotNull === "1" ? " NOT NULL" : "";
+ return `Type: ${columnType.toUpperCase()}${notNull}`;
+}
+
+function canChooseColumns() {
+ return !!(
+ document.querySelector("column-chooser") && window._columnChooserData
+ );
+}
+
+function shouldShowShowAllColumns() {
+ var params = getParams();
+ return params.getAll("_nocol").length || params.getAll("_col").length;
+}
+
+function buildColumnActionItems(manager, th, options) {
+ options = options || {};
+ var params = getParams();
+ var column = th.dataset.column;
+ var columnActions = [];
+ var isSortable = !!th.querySelector("a");
+ var isFirstColumn = th.parentElement.querySelector("th:first-of-type") === th;
+ var isSinglePk =
+ th.dataset.isPk === "1" &&
+ document.querySelectorAll('th[data-is-pk="1"]').length === 1;
+ var hasBlankValues = getColumnCells(th).some(
+ (el) => el.innerText.trim() === "",
+ );
+
+ if (isSortable && params.get("_sort") !== column) {
+ columnActions.push({
+ label: "Sort ascending",
+ href: sortAscUrl(column),
+ });
+ }
+
+ if (isSortable && params.get("_sort_desc") !== column) {
+ columnActions.push({
+ label: "Sort descending",
+ href: sortDescUrl(column),
+ });
+ }
+
+ if (
+ DATASETTE_ALLOW_FACET &&
+ !isFirstColumn &&
+ !getDisplayedFacets().includes(column) &&
+ !isSinglePk
+ ) {
+ columnActions.push({
+ label: "Facet by this",
+ href: facetUrl(column),
+ });
+ }
+
+ if (options.includeChooseColumns && canChooseColumns()) {
+ columnActions.push({
+ label: "Choose columns",
+ href: "#",
+ onClick:
+ options.onChooseColumns ||
+ function (ev) {
+ ev.preventDefault();
+ openColumnChooser();
+ },
+ });
+ }
+
+ if (th.dataset.isPk !== "1") {
+ columnActions.push({
+ label: "Hide this column",
+ href: hideColumnUrl(column),
+ });
+ }
+
+ if (options.includeShowAllColumns && shouldShowShowAllColumns()) {
+ columnActions.push({
+ label: "Show all columns",
+ href: showAllColumnsUrl(),
+ });
+ }
+
+ if (params.get(`${column}__notblank`) !== "1" && hasBlankValues) {
+ columnActions.push({
+ label: "Show not-blank rows",
+ href: notBlankUrl(column),
+ });
+ }
+
+ return columnActions.concat(manager.makeColumnActions(getColumnMeta(th)));
+}
+
+function buildColumnActionState(manager, th, options) {
+ return {
+ column: th.dataset.column,
+ columnDescription: th.dataset.columnDescription || null,
+ columnMeta: getColumnMeta(th),
+ columnTypeText: getColumnTypeText(th),
+ actionItems: buildColumnActionItems(manager, th, options),
+ };
+}
+
+function initializeColumnActions(manager) {
+ manager.columnActions = {
+ buildColumnActionState: function (th, options) {
+ return buildColumnActionState(manager, th, options);
+ },
+ buildColumnActionItems: function (th, options) {
+ return buildColumnActionItems(manager, th, options);
+ },
+ canChooseColumns: canChooseColumns,
+ facetUrl: facetUrl,
+ getColumnMeta: getColumnMeta,
+ getColumnTypeText: getColumnTypeText,
+ hideColumnUrl: hideColumnUrl,
+ notBlankUrl: notBlankUrl,
+ shouldShowShowAllColumns: shouldShowShowAllColumns,
+ showAllColumnsUrl: showAllColumnsUrl,
+ sortAscUrl: sortAscUrl,
+ sortDescUrl: sortDescUrl,
+ };
+}
+
+function renderActionLink(itemConfig) {
+ var newLink = document.createElement("a");
+ newLink.textContent = itemConfig.label;
+ newLink.href = itemConfig.href || "#";
+ if (itemConfig.onClick) {
+ newLink.addEventListener("click", itemConfig.onClick);
+ }
+ return newLink;
+}
+
/** 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");
@@ -97,100 +265,34 @@ const initDatasetteTable = function (manager) {
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");
- var selectColumns = menu.querySelector("a.dropdown-choose-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";
- }
- /* Choose columns - show if web component exists */
- var columnChooser = document.querySelector("column-chooser");
- if (columnChooser && window._columnChooserData) {
- selectColumns.parentNode.style.display = "block";
- selectColumns.addEventListener("click", function (ev) {
+ var actionState = manager.columnActions.buildColumnActionState(th, {
+ includeChooseColumns: true,
+ includeShowAllColumns: true,
+ onChooseColumns: function (ev) {
ev.preventDefault();
closeMenu();
openColumnChooser();
- });
- } else {
- selectColumns.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" : "";
+ },
+ });
+ var menuList = menu.querySelector("ul.dropdown-actions");
+ menuList.innerHTML = "";
+ actionState.actionItems.forEach((itemConfig) => {
+ var menuItem = document.createElement("li");
+ menuItem.appendChild(renderActionLink(itemConfig));
+ menuList.appendChild(menuItem);
+ });
- if (columnType) {
+ var columnTypeP = menu.querySelector(".dropdown-column-type");
+ if (actionState.columnTypeText) {
columnTypeP.style.display = "block";
- columnTypeP.innerText = `Type: ${columnType.toUpperCase()}${notNull}`;
+ columnTypeP.innerText = actionState.columnTypeText;
} else {
columnTypeP.style.display = "none";
}
var columnDescriptionP = menu.querySelector(".dropdown-column-description");
- if (th.dataset.columnDescription) {
- columnDescriptionP.innerText = th.dataset.columnDescription;
+ if (actionState.columnDescription) {
+ columnDescriptionP.innerText = actionState.columnDescription;
columnDescriptionP.style.display = "block";
} else {
columnDescriptionP.style.display = "none";
@@ -201,39 +303,6 @@ const initDatasetteTable = function (manager) {
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;
@@ -383,7 +452,7 @@ function openColumnChooser() {
}
var qs = params.toString();
location.href = qs ? "?" + qs : location.pathname;
- }
+ },
});
}
@@ -391,11 +460,12 @@ function openColumnChooser() {
document.addEventListener("datasette_init", function (evt) {
const { detail: manager } = evt;
+ initializeColumnActions(manager);
+
// Main table
initDatasetteTable(manager);
// Other UI functions with interactive JS needs
addButtonsToFilterRows(manager);
initAutocompleteForFilterValues(manager);
-
});
diff --git a/datasette/templates/table.html b/datasette/templates/table.html
index 9c930918..0df08a94 100644
--- a/datasette/templates/table.html
+++ b/datasette/templates/table.html
@@ -6,6 +6,7 @@
{{- super() -}}
+