Skip to content

Instantly share code, notes, and snippets.

@swanson

swanson/table_scroll.html.erb Secret

Last active Feb 10, 2021
Embed
What would you like to do?
<div data-controller="table-scroll"
data-table-scroll-nav-shown-class="flex"
data-table-scroll-nav-hidden-class="hidden"
data-table-scroll-button-disabled-class="text-gray-200"
data-table-scroll-indicator-visible-class="text-blue-600"
>
<div class="flex items-center justify-center space-x-2 py-3 text-gray-600" data-table-scroll-target="navBar">
<%= button_tag data: { table_scroll_target: "leftButton", action: "table-scroll#scrollLeft" } do %>
<%= svg_icon("left-arrow", class: "h-5") %>
<% end %>
<% 5.times do %>
<%= svg_icon("dot", class: "h-2 text-gray-200", "data-table-scroll-target": "columnVisibilityIndicator") %>
<% end %>
<%= button_tag data: { table_scroll_target: "rightButton", action: "table-scroll#scrollRight" } do %>
<%= svg_icon("right-arrow", class: "h-5") %>
<% end %>
</div>
<div class="flex flex-col mx-auto">
<div class="overflow-x-auto" data-table-scroll-target="scrollArea">
<table class="min-w-full">
<thead class="bg-blue-800 text-white">
<tr>
<th class="table-header" data-table-scroll-target="column">Product</th>
<th class="table-header" data-table-scroll-target="column">Price</th>
<th class="table-header" data-table-scroll-target="column">SKU</th>
<th class="table-header" data-table-scroll-target="column">Sold</th>
<th class="table-header" data-table-scroll-target="column">Net Sales</th>
</tr>
</thead>
<tbody class="divide-y">
<% @products.each_with_index do |p, idx| %>
<%= tag.tr(class: { "bg-white": idx.even?, "bg-gray-50": idx.odd? }) do %>
<td class="table-cell font-bold tracking-wide">
<%= p.name %>
</td>
<td class="table-cell">
<%= number_to_currency(p.price / 100.0) %>
</td>
<td class="table-cell">
<%= p.sku %>
</td>
<td class="table-cell">
<%= p.quantity %>
</td>
<td class="table-cell">
<%= number_to_currency(p.net_sales / 100.0) %>
</td>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
import { Controller } from "stimulus";
function supportsIntersectionObserver() {
return (
"IntersectionObserver" in window ||
"IntersectionObserverEntry" in window ||
"intersectionRatio" in window.IntersectionObserverEntry.prototype
);
}
export default class extends Controller {
static targets = [
"navBar",
"scrollArea",
"column",
"leftButton",
"rightButton",
"columnVisibilityIndicator",
];
static classes = [
"navShown",
"navHidden",
"buttonDisabled",
"indicatorVisible",
];
connect() {
this.startObservingColumnVisibility();
}
disconnect() {
this.stopObservingColumnVisibility();
}
startObservingColumnVisibility() {
if (!supportsIntersectionObserver()) {
console.warn(`This browser doesn't support IntersectionObserver`);
return;
}
this.intersectionObserver = new IntersectionObserver(
this.updateScrollNavigation.bind(this),
{
root: this.scrollAreaTarget,
threshold: 0.99, // otherwise, the right-most column sometimes won't be considered visible in some browsers, rounding errors, etc.
}
);
this.columnTargets.forEach((headingEl) => {
this.intersectionObserver.observe(headingEl);
});
}
stopObservingColumnVisibility() {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
}
updateScrollNavigation(observerRecords) {
observerRecords.forEach((record) => {
record.target.dataset.isVisible = record.isIntersecting;
});
this.toggleScrollNavigationVisibility();
this.updateColumnVisibilityIndicators();
this.updateLeftRightButtonAffordance();
}
toggleScrollNavigationVisibility() {
const allColumnsVisible =
this.columnTargets.length > 0 &&
this.columnTargets[0].dataset.isVisible === "true" &&
this.columnTargets[this.columnTargets.length - 1].dataset.isVisible ===
"true";
if (allColumnsVisible) {
this.navBarTarget.classList.remove(this.navShownClass);
this.navBarTarget.classList.add(this.navHiddenClass);
} else {
this.navBarTarget.classList.add(this.navShownClass);
this.navBarTarget.classList.remove(this.navHiddenClass);
}
}
updateColumnVisibilityIndicators() {
this.columnTargets.forEach((headingEl, index) => {
const indicator = this.columnVisibilityIndicatorTargets[index];
if (indicator) {
indicator.classList.toggle(
this.indicatorVisibleClass,
headingEl.dataset.isVisible === "true"
);
}
});
}
updateLeftRightButtonAffordance() {
const firstColumnHeading = this.columnTargets[0];
const lastColumnHeading = this.columnTargets[this.columnTargets.length - 1];
this.updateButtonAffordance(
this.leftButtonTarget,
firstColumnHeading.dataset.isVisible === "true"
);
this.updateButtonAffordance(
this.rightButtonTarget,
lastColumnHeading.dataset.isVisible === "true"
);
}
updateButtonAffordance(button, isDisabled) {
if (isDisabled) {
button.setAttribute("disabled", "");
button.classList.add(this.buttonDisabledClass);
} else {
button.removeAttribute("disabled");
button.classList.remove(this.buttonDisabledClass);
}
}
scrollLeft() {
// scroll to make visible the first non-fully-visible column to the left of the scroll area
let columnToScrollTo = null;
for (let i = 0; i < this.columnTargets.length; i++) {
const column = this.columnTargets[i];
if (columnToScrollTo !== null && column.dataset.isVisible === "true") {
break;
}
if (column.dataset.isVisible === "false") {
columnToScrollTo = column;
}
}
this.scrollAreaTarget.scroll(columnToScrollTo.offsetLeft, 0);
}
scrollRight() {
// scroll to make visible the first non-fully-visible column to the right of the scroll area
let columnToScrollTo = null;
for (let i = this.columnTargets.length - 1; i >= 0; i--) {
// right to left
const column = this.columnTargets[i];
if (columnToScrollTo !== null && column.dataset.isVisible === "true") {
break;
}
if (column.dataset.isVisible === "false") {
columnToScrollTo = column;
}
}
this.scrollAreaTarget.scroll(columnToScrollTo.offsetLeft, 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment