diff --git a/datasette/static/app.css b/datasette/static/app.css index f7cd97b0..ce800f61 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -609,9 +609,6 @@ button.core[type=button] { border-color: #007bff; } -.filter-row { - margin-bottom: 0.6em; -} .search-row { margin-bottom: 1.8em; } @@ -623,72 +620,239 @@ button.core[type=button] { width: 80px; } -.select-wrapper { - border: 1px solid #ccc; - width: 120px; - border-radius: 3px; +.filters .search-row { + box-sizing: border-box; + display: grid; + grid-template-columns: max-content minmax(16rem, 1fr); + align-items: center; + gap: 0.6rem; + margin: 0 0 0.45rem; + padding-right: var(--filter-two-icon-space); +} + +.filters .search-row label { + width: auto; padding: 0; - background-color: #fafafa; - position: relative; + color: var(--filter-muted); + font-size: 0.875rem; + font-weight: 600; +} + +.filters { + --filter-ink: #0f0f0f; + --filter-paper: #eef6ff; + --filter-muted: #6b6b6b; + --filter-rule: #d8e6f5; + --filter-accent: #1a56db; + --filter-control-border: #bfccd9; + --filter-control-height: 2.125rem; + --filter-control-gap: 0.4rem; + --filter-row-icon-size: 2rem; + --filter-two-icon-space: calc( + (2 * var(--filter-row-icon-size)) + (2 * var(--filter-control-gap)) + ); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0.55rem; + max-width: 760px; + margin: 0 0 1rem; + padding: 0.75rem; + border: 1px solid var(--filter-rule); + border-radius: 8px; + background: var(--filter-paper); + color: var(--filter-ink); + font-size: 0.875rem; +} + +.filters .filter-row { + margin-bottom: 0; +} + +.filters .filter-controls-row { + display: grid; + min-width: 0; + width: 100%; + max-width: 100%; + box-sizing: border-box; + grid-template-columns: + minmax(8rem, 0.75fr) + minmax(6.5rem, 0.5fr) + minmax(12rem, 1.2fr) + var(--filter-row-icon-size) + var(--filter-row-icon-size); + gap: var(--filter-control-gap); + align-items: center; +} + +.filters .filter-actions-row { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: 0.6rem; +} + +.select-wrapper { display: inline-block; - margin-right: 0.3em; -} -.select-wrapper:focus-within { - border: 1px solid black; + width: 120px; + min-width: 0; } + .select-wrapper.filter-op { width: 80px; } -.select-wrapper::after { - content: "\25BE"; - position: absolute; - top: 0px; - right: 0.4em; - color: #bbb; - pointer-events: none; - font-size: 1.2em; - padding-top: 0.16em; + +.filters .select-wrapper { + width: auto; } .select-wrapper select { - padding: 9px 8px; + box-sizing: border-box; width: 100%; - border: none; - box-shadow: none; - background: transparent; - background-image: none; - -webkit-appearance: none; - -moz-appearance: none; + height: var(--filter-control-height); + border: 1px solid var(--filter-control-border, #ccc); + border-radius: 5px; + background-color: #fff; + color: inherit; cursor: pointer; + font-family: inherit; + font-size: 0.875rem; + line-height: 1.25; + padding: 7px 8px; } -.select-wrapper select { - font-size: 1em; - font-family: Helvetica, sans-serif; -} + .select-wrapper option { font-size: 1em; font-family: Helvetica, sans-serif; } .select-wrapper select:focus { + border-color: var(--filter-accent, #000); + box-shadow: 0 0 0 2px rgba(26, 86, 219, 0.14); outline: none; } -.filters { - font-size: 0.8em; -} .filters input.filter-value { - width: 200px; - border-radius: 3px; - -webkit-appearance: none; - padding: 9px 4px; - font-size: 16px; - font-family: Helvetica, sans-serif; + box-sizing: border-box; + width: 100%; + min-width: 0; + height: var(--filter-control-height); + border: 1px solid var(--filter-control-border); + border-radius: 5px; + background: #fff; + color: inherit; + font-family: inherit; + font-size: 0.875rem; + line-height: 1.25; + padding: 7px 9px; +} + +.filters input.filter-value:focus { + border-color: var(--filter-accent); + box-shadow: 0 0 0 2px rgba(26, 86, 219, 0.14); + outline: none; +} + +.filters input[type=submit] { + border-color: var(--filter-accent); + background: var(--filter-accent); + border-radius: 5px; + font-weight: 500; + padding: 0.55rem 0.85rem; +} + +.filters input[type=submit]:hover, +.filters input[type=submit]:focus { + background: #1949b8; + border-color: #1949b8; +} + +.filters button.filter-row-icon { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: var(--filter-row-icon-size); + height: var(--filter-row-icon-size); + min-width: var(--filter-row-icon-size); + padding: 0; + border: 1px solid var(--filter-rule); + border-radius: 5px; + background: #fff; + color: var(--filter-muted); + font-family: inherit; + font-size: 1.15rem; + font-weight: 600; + line-height: 1; +} + +.filters button.filter-row-icon[hidden] { + display: none; +} + +.filters button.filter-row-icon:focus-visible { + outline: 3px solid rgba(26, 86, 219, 0.14); + outline-offset: 2px; +} + +.filters .filter-row-remove-icon { + display: block; + height: 14px; + width: 14px; +} + +.filters button.filter-row-remove:hover, +.filters button.filter-row-remove:focus { + border-color: #c9d5e3; + background: #f8fbff; + color: var(--filter-ink); +} + +.filters button.filter-row-add { + border-color: var(--filter-accent); + background: var(--filter-accent); + color: #fff; +} + +.filters .filter-row-add-icon { + display: block; + height: 16px; + width: 16px; +} + +.filters button.filter-row-add:hover, +.filters button.filter-row-add:focus { + border-color: #1949b8; + background: #1949b8; + color: #fff; +} + +.filters button.filter-row-add:focus-visible svg { + color: #fff; + stroke: currentColor; } #_search { font-size: 16px; } +.filters #_search { + box-sizing: border-box; + width: 100%; + height: var(--filter-control-height); + border: 1px solid var(--filter-control-border); + border-radius: 5px; + background: #fff; + color: var(--filter-ink); + font: inherit; + padding: 7px 9px; +} + +.filters #_search:focus { + border-color: var(--filter-accent); + box-shadow: 0 0 0 2px rgba(26, 86, 219, 0.14); + outline: none; +} + @@ -3021,14 +3185,56 @@ select.table-alter-input { margin-bottom: 0.35rem; } - .select-wrapper { - width: 100px; + .filters { + max-width: none; + padding: 0.65rem; } - .select-wrapper.filter-op { - width: 60px; + .filters .search-row { + grid-template-columns: max-content minmax(0, 1fr); + padding-right: 0; + } + .filters .filter-controls-row { + --filter-value-button-space: 0px; + --filter-remove-button-offset: 0px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .filters .filter-controls-row-one-button { + --filter-value-button-space: calc( + var(--filter-row-icon-size) + var(--filter-control-gap) + ); + } + .filters .filter-controls-row-two-buttons { + --filter-value-button-space: var(--filter-two-icon-space); + --filter-remove-button-offset: calc( + var(--filter-row-icon-size) + var(--filter-control-gap) + ); + } + .filters .filter-controls-row .select-wrapper { + grid-row: 1; + grid-column: 1 / 2; + } + .filters .filter-controls-row .select-wrapper.filter-op { + grid-column: 2 / 3; } .filters input.filter-value { - width: 140px; + grid-row: 2; + grid-column: 1 / 3; + justify-self: start; + width: calc(100% - var(--filter-value-button-space)); + } + .filters button.filter-row-icon { + grid-row: 2; + grid-column: 1 / 3; + justify-self: end; + } + .filters .filter-controls-row-two-buttons button.filter-row-remove:not([hidden]) { + margin-right: var(--filter-remove-button-offset); + } + .filters .filter-actions-row { + justify-content: flex-start; + } + .filters .filter-actions-row input[type=submit] { + flex: 0 1 auto; } button.choose-columns-mobile, button.table-insert-row, diff --git a/datasette/static/table.js b/datasette/static/table.js index f160f3f3..74a96d8e 100644 --- a/datasette/static/table.js +++ b/datasette/static/table.js @@ -633,32 +633,151 @@ const initDatasetteTable = function (manager) { }); }; -/* Add x buttons to the filter rows */ -function addButtonsToFilterRows(manager) { - var x = "✖"; - var rows = Array.from( - document.querySelectorAll(manager.selectors.filterRow), +function filterRowSelector(manager) { + return manager.selectors.filterRows || manager.selectors.filterRow; +} + +function filterRowsWithControls(manager) { + return Array.from( + document.querySelectorAll(filterRowSelector(manager)), ).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); +} + +function filterRowNumberFromName(name) { + var match = name && name.match(/^_filter_column_(\d+)$/); + return match ? parseInt(match[1], 10) : 0; +} + +function nextFilterRowNumber(manager) { + return filterRowsWithControls(manager).reduce((max, row) => { var column = row.querySelector("select"); - if (!column.value) { - a.style.display = "none"; + return Math.max(max, filterRowNumberFromName(column && column.name)); + }, 0) + 1; +} + +function setFilterRowNumber(row, number) { + row.querySelector("select").name = `_filter_column_${number}`; + row.querySelector(".filter-op select").name = `_filter_op_${number}`; + row.querySelector("input.filter-value").name = `_filter_value_${number}`; +} + +function resetFilterRow(row) { + row.querySelector("select").value = ""; + row.querySelector(".filter-op select").value = "exact"; + row.querySelector("input.filter-value").value = ""; +} + +function updateFilterRowButtons(manager) { + var rows = filterRowsWithControls(manager); + rows.forEach((row, index) => { + var removeButton = row.querySelector(".filter-row-remove"); + var addButton = row.querySelector(".filter-row-add"); + var column = row.querySelector("select"); + if (removeButton) { + removeButton.hidden = index === 0; + } + if (addButton) { + addButton.hidden = index !== rows.length - 1 || !column.value; + } + var visibleButtonCount = [removeButton, addButton].filter(function (button) { + return button && !button.hidden; + }).length; + row.classList.toggle( + "filter-controls-row-has-buttons", + visibleButtonCount > 0, + ); + row.classList.toggle( + "filter-controls-row-one-button", + visibleButtonCount === 1, + ); + row.classList.toggle( + "filter-controls-row-two-buttons", + visibleButtonCount === 2, + ); + }); +} + +function cloneFilterRow(row) { + var clone = row.cloneNode(true); + clone.querySelector("select").name = "_filter_column"; + clone.querySelector(".filter-op select").name = "_filter_op"; + clone.querySelector("input.filter-value").name = "_filter_value"; + resetFilterRow(clone); + clone.querySelectorAll(".filter-row-icon").forEach((button) => button.remove()); + return clone; +} + +var FILTER_REMOVE_ICON_SVG = ``; + +var FILTER_ADD_ICON_SVG = ``; + +function addFilterRowButtons(row, manager) { + var removeButton = document.createElement("button"); + removeButton.type = "button"; + removeButton.className = "filter-row-icon filter-row-remove"; + removeButton.setAttribute("aria-label", "Remove this filter"); + removeButton.title = "Remove this filter"; + removeButton.tabIndex = 0; + removeButton.innerHTML = FILTER_REMOVE_ICON_SVG; + removeButton.addEventListener("click", (ev) => { + var row = ev.currentTarget.closest(filterRowSelector(manager)); + var rows = filterRowsWithControls(manager); + var rowIndex = rows.indexOf(row); + var focusRow = rows[rowIndex + 1] || rows[rowIndex - 1] || null; + row.remove(); + updateFilterRowButtons(manager); + if (focusRow) { + var focusTarget = + focusRow.querySelector(".filter-row-add:not([hidden])") || + focusRow.querySelector("select"); + if (focusTarget) { + focusTarget.focus(); + } } }); + row.appendChild(removeButton); + + var addButton = document.createElement("button"); + addButton.type = "button"; + addButton.className = "filter-row-icon filter-row-add"; + addButton.setAttribute("aria-label", "Add another filter"); + addButton.title = "Add another filter"; + addButton.tabIndex = 0; + addButton.innerHTML = FILTER_ADD_ICON_SVG; + addButton.addEventListener("click", (ev) => { + var row = ev.currentTarget.closest(filterRowSelector(manager)); + if (row.querySelector("select").name === "_filter_column") { + setFilterRowNumber(row, nextFilterRowNumber(manager)); + } + var clone = cloneFilterRow(row); + addFilterRowButtons(clone, manager); + row.parentNode.insertBefore(clone, row.nextSibling); + updateFilterRowButtons(manager); + clone.querySelector("select").focus(); + }); + row.appendChild(addButton); + + row.querySelector("select").addEventListener("change", () => { + updateFilterRowButtons(manager); + }); +} + +/* Add buttons to the filter rows */ +function addButtonsToFilterRows(manager) { + var rows = filterRowsWithControls(manager); + rows.forEach((row) => { + addFilterRowButtons(row, manager); + }); + updateFilterRowButtons(manager); } /* Set up datalist autocomplete for filter values */ @@ -687,11 +806,11 @@ function initAutocompleteForFilterValues(manager) { }); } createDataLists(); - // When any select with name=_filter_column changes, update the datalist + // When any filter column select changes, update the datalist document.body.addEventListener("change", function (event) { - if (event.target.name === "_filter_column") { + if (event.target.name && event.target.name.startsWith("_filter_column")) { event.target - .closest(manager.selectors.filterRow) + .closest(filterRowSelector(manager)) .querySelector(".filter-value") .setAttribute("list", "datalist-" + event.target.value); } diff --git a/datasette/templates/patterns.html b/datasette/templates/patterns.html index a46478a7..bff44341 100644 --- a/datasette/templates/patterns.html +++ b/datasette/templates/patterns.html @@ -202,9 +202,9 @@

3 rows where characteristic_id = 2

-
+
-
+
-
+
-
-
+
+
- - + +
diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 86b6a6ed..e06ef94e 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -55,12 +55,12 @@ {% endif %} -
+ {% if supports_search %}
{% endif %} {% for column, lookup, value in filters.selections() %} -
+
{% endfor %} -
+
-
+
{% if is_sortable %} -
+
- + {% endif %} {% for key, value in form_hidden_args %} {% endfor %} - +