active_admin_prism 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/js-src/prism.js CHANGED
@@ -121,6 +121,24 @@
121
121
  event.preventDefault();
122
122
  setCollapsed(toggle, !document.body.classList.contains("prism-sidebar-collapsed"));
123
123
  });
124
+
125
+ // Clicking into the nav while collapsed to the icon-only rail expands
126
+ // it back out — the collapsed rail exists to save width while
127
+ // *browsing* the current page, not as a standing preference that
128
+ // should keep the destination page's own labels (or, for a parent
129
+ // item, its whole submenu) hidden too. Covers both a leaf link (real
130
+ // `<a href>`, about to navigate away) and a parent's own
131
+ // ".prism-nav-group-toggle" — that one never navigates, but while
132
+ // collapsed its submenu is force-hidden regardless of the "open"
133
+ // class the sidebar's own separate click handler still toggles on it
134
+ // (scss-src/_sidebar.scss's ".prism-nav-submenu { display: none
135
+ // !important; }" under body.prism-sidebar-collapsed), so without
136
+ // this, clicking a parent like "School" silently did nothing visible.
137
+ document.addEventListener("click", function (event) {
138
+ if (!document.body.classList.contains("prism-sidebar-collapsed")) return;
139
+ if (!event.target.closest("a.prism-nav-link, .prism-nav-group-toggle")) return;
140
+ setCollapsed(toggle, false);
141
+ });
124
142
  });
125
143
  })();
126
144
 
@@ -352,73 +370,248 @@
352
370
  });
353
371
  })();
354
372
 
355
- // "Filters" sidebar panel: collapses to a single icon button, expanding to
356
- // the full form on click. See scss-src/_panels.scss (#filters_sidebar_section)
357
- // for the actual collapse/expand styling this only toggles the ".open"
358
- // class an inline click responds to. Skipped entirely when
373
+ // Shared by the "consolidated sidebar panels" (below) and "consolidated
374
+ // action items" (further down) features both are independent top-level
375
+ // IIFEs, so this is their one shared meeting point rather than a closure:
376
+ // each wants its own Select2 "jump menu" sitting in the *same* row, in
377
+ // the title bar, rather than the sidebar one living separately down in
378
+ // #sidebar. Whichever feature's DOMContentLoaded handler runs first
379
+ // creates the row; the other just finds and reuses it. Declared with
380
+ // `function` (not `const`/an IIFE-local var) so it's hoisted and visible
381
+ // to every IIFE below it in this same file regardless of registration
382
+ // order.
383
+ function prismTitlebarActionsRow() {
384
+ var right = document.getElementById("titlebar_right");
385
+ if (!right) return null;
386
+
387
+ var row = document.getElementById("prism-titlebar-actions-row");
388
+ if (row) return row;
389
+
390
+ var actionItems = right.querySelector(":scope > .action_items") || right;
391
+
392
+ row = document.createElement("span");
393
+ row.id = "prism-titlebar-actions-row";
394
+ row.className = "prism-titlebar-actions-row";
395
+ actionItems.appendChild(row);
396
+
397
+ // ActiveAdmin's own auto-added "New <Resource>" button (marked
398
+ // ".prism-action-item-new" — see lib/active_admin/views/action_items.rb)
399
+ // is excluded from consolidation below (it's the single most-used
400
+ // action on an index page, unlike the bulk-action/CSV-upload links this
401
+ // feature actually exists to tidy away) — re-appended here, after the
402
+ // row, so it keeps its traditional spot at the very top-right corner
403
+ // (text-align:right on #titlebar_right means the *last* child sits at
404
+ // the true right edge) with the consolidated dropdown(s) landing just
405
+ // to its left instead of displacing it. Harmless no-op if there's no
406
+ // such button on this page (a custom `register_page`, say).
407
+ var newItem = actionItems.querySelector(":scope > .action_item.prism-action-item-new");
408
+ if (newItem) actionItems.appendChild(newItem);
409
+
410
+ return row;
411
+ }
412
+
413
+ // Collapsible sidebar sections: collapse to a single icon button by
414
+ // default, expanding to the full panel on click. See
415
+ // scss-src/_panels.scss (.prism-collapsible-panel) for the actual
416
+ // collapse/expand styling — this only toggles a section's own ".open"
417
+ // class an inline click responds to. Applies to Filters this way exactly
418
+ // as it always has; any *other* collapsible section (a host's own
419
+ // `sidebar "Title" do ... end` block) never actually opens in place at
420
+ // all — buildOtherSectionsDropdown below immediately flattens its
421
+ // links into the "Sidebar Actions" dropdown and discards the rest of the
422
+ // section outright, so the wireUp() call each one still gets here (for
423
+ // consistency/simplicity, not because it matters) attaches to an element
424
+ // that's about to be removed. Skipped entirely when
359
425
  // ActiveAdminPrism.configuration.collapsible_filters is false (the
360
426
  // "prism-filters-collapsible-disabled" body class, set in
361
427
  // lib/active_admin/views/flash_messages.rb#body_classes).
362
428
  (function () {
363
429
  "use strict";
364
430
 
365
- document.addEventListener("DOMContentLoaded", function () {
366
- if (document.body.classList.contains("prism-filters-collapsible-disabled")) return;
367
-
368
- var section = document.getElementById("filters_sidebar_section");
369
- if (!section) return;
431
+ function setOpen(section, toggle, open) {
432
+ section.classList.toggle("open", open);
433
+ toggle.setAttribute("aria-expanded", open ? "true" : "false");
434
+ // The Filters section specifically also mirrors its state onto
435
+ // <body> — #main_content (the table/grid area) is a *sibling* of
436
+ // #sidebar, not a descendant of this section, so CSS can't reach
437
+ // across siblings any other way. scss-src/_panels.scss's own
438
+ // #sidebar width reflow no longer needs this (that rule now reacts
439
+ // to ANY section's ".open" via ":has()" instead of this one
440
+ // specifically), but a host's own CSS may already key off this
441
+ // documented class, so it's kept.
442
+ if (section.id === "filters_sidebar_section") {
443
+ document.body.classList.toggle("prism-filters-open", open);
444
+ }
445
+ }
370
446
 
447
+ function wireUp(section) {
371
448
  var toggle = section.querySelector(":scope > h3");
372
- if (!toggle) return;
449
+ if (!toggle) return null;
373
450
 
374
451
  toggle.setAttribute("role", "button");
375
452
  toggle.setAttribute("tabindex", "0");
376
453
  toggle.setAttribute("aria-expanded", section.classList.contains("open") ? "true" : "false");
377
- // Mirrored onto <body> because #main_content (the table/grid area)
378
- // is a *sibling* of #sidebar, not a descendant of this section — CSS
379
- // can't reach across siblings, so scss-src/_panels.scss keys the
380
- // #main_content/#sidebar width reflow off this body class instead.
381
- document.body.classList.toggle("prism-filters-open", section.classList.contains("open"));
382
-
383
- function setOpen(open) {
384
- section.classList.toggle("open", open);
385
- toggle.setAttribute("aria-expanded", open ? "true" : "false");
386
- document.body.classList.toggle("prism-filters-open", open);
387
- }
388
454
 
389
- function toggleOpen() {
390
- setOpen(!section.classList.contains("open"));
391
- }
392
-
393
- toggle.addEventListener("click", toggleOpen);
455
+ toggle.addEventListener("click", function () {
456
+ setOpen(section, toggle, !section.classList.contains("open"));
457
+ });
394
458
  toggle.addEventListener("keydown", function (event) {
395
459
  if (event.key !== "Enter" && event.key !== " ") return;
396
460
  event.preventDefault();
397
- toggleOpen();
461
+ setOpen(section, toggle, !section.classList.contains("open"));
462
+ });
463
+
464
+ return toggle;
465
+ }
466
+
467
+ // Every OTHER collapsible section — anything besides Filters, e.g. a
468
+ // host's own custom `sidebar "..." do ... end` block — gets consolidated
469
+ // into a single "Sidebar Actions" Select2 dropdown instead of each
470
+ // rendering its own individually collapsed icon button; every
471
+ // link/button inside is flattened into that dropdown as its own option
472
+ // (see buildOtherSectionsDropdown below), and the section itself is
473
+ // discarded outright rather than ever opening in place. Filters is
474
+ // deliberately excluded from this and always keeps its own plain icon
475
+ // button exactly as before, whether or not any other sections exist —
476
+ // see ActiveAdminPrism::Configuration#collapsible_filters.
477
+ //
478
+ // Rendered in the title bar (prismTitlebarActionsRow, shared with the
479
+ // consolidated action_items dropdown below) rather than inside #sidebar
480
+ // itself — sitting next to that dropdown, styled the same way, reads as
481
+ // one coherent "more stuff lives here" row rather than two separate,
482
+ // disconnected mechanisms in different parts of the page.
483
+ //
484
+ // Deliberately flattened to match that other dropdown's own click-and-
485
+ // reset "jump menu" flow instead of a nested reveal-a-panel-then-pick-
486
+ // a-link one: every actual link/button/submit inside every non-Filters
487
+ // section's content becomes its own flat option here (a section that
488
+ // groups a few related links, the common case, disappears entirely as
489
+ // a grouping level) — moved into a hidden holding area, same technique
490
+ // and same reason as .prism-action-items-holding below (an element
491
+ // fully removed from the document can't bubble a click up to where
492
+ // Rails UJS's own document-level listener is, breaking any
493
+ // data-method/data-confirm link; staying attached-but-hidden avoids
494
+ // that). Each non-Filters section itself (now an empty shell once its
495
+ // content's been moved out) is discarded outright either way, so
496
+ // #sidebar never has to make room for anything beyond Filters alone.
497
+ function buildOtherSectionsDropdown(entries) {
498
+ if (typeof jQuery === "undefined" || !jQuery.fn.select2) return;
499
+
500
+ var row = prismTitlebarActionsRow();
501
+ if (!row) return;
502
+
503
+ var holding = document.createElement("div");
504
+ holding.className = "prism-sidebar-panel-holding";
505
+ holding.hidden = true;
506
+
507
+ var labels = [];
508
+ entries.forEach(function (entry) {
509
+ var contents = entry.section.querySelector(":scope > .panel_contents");
510
+ if (contents) {
511
+ Array.prototype.forEach.call(contents.querySelectorAll("a, button, input[type=submit]"), function (trigger) {
512
+ trigger.dataset.prismSidebarActionsIndex = String(labels.length);
513
+ labels.push((trigger.value || trigger.textContent || "").trim());
514
+ });
515
+ holding.appendChild(contents);
516
+ }
517
+ entry.section.remove();
398
518
  });
399
519
 
520
+ if (!labels.length) return;
521
+
522
+ var wrapper = document.createElement("span");
523
+ wrapper.className = "prism-sidebar-panel-select-wrapper";
524
+
525
+ var select = document.createElement("select");
526
+ select.className = "prism-sidebar-panel-select";
527
+ // A real leading <option value=""> (kept selected, non-removable) is
528
+ // what lets Select2 show a placeholder on a *single* select at all.
529
+ select.appendChild(document.createElement("option"));
530
+
531
+ labels.forEach(function (label, index) {
532
+ var option = document.createElement("option");
533
+ option.value = String(index);
534
+ option.textContent = label;
535
+ select.appendChild(option);
536
+ });
537
+
538
+ wrapper.appendChild(select);
539
+ wrapper.appendChild(holding);
540
+ // Appended, not inserted first — whichever of this feature or the
541
+ // action_items one below runs first (DOMContentLoaded handlers fire
542
+ // in registration order, and this file registers this one first)
543
+ // claims the left slot in the row; the other lands to its right.
544
+ row.appendChild(wrapper);
545
+
546
+ var $select = jQuery(select).select2({
547
+ width: "220px",
548
+ placeholder: "Sidebar Actions (" + labels.length + ")",
549
+ // 0, not some higher threshold: always show the search box, even
550
+ // with only a couple of options — that's the one visual cue that
551
+ // reads unambiguously as "this is Select2, not a plain dropdown"
552
+ // regardless of how many links a host's sidebar block happens to
553
+ // have right now, and it's what keeps this usable once it does
554
+ // grow rather than needing to cross a threshold first.
555
+ minimumResultsForSearch: 0
556
+ });
557
+
558
+ $select.on("select2:select", function (event) {
559
+ var index = event.params.data.id;
560
+ var trigger = holding.querySelector('[data-prism-sidebar-actions-index="' + index + '"]');
561
+ if (trigger) trigger.click();
562
+ // Resets to the placeholder right away, same as the action_items
563
+ // dropdown — this fires an action, it isn't a persistent selection.
564
+ $select.val("").trigger("change");
565
+ });
566
+ }
567
+
568
+ document.addEventListener("DOMContentLoaded", function () {
569
+ if (document.body.classList.contains("prism-filters-collapsible-disabled")) return;
570
+
571
+ var filtersSection = document.getElementById("filters_sidebar_section");
572
+ var filtersToggle = null;
573
+ var otherEntries = [];
574
+
575
+ document.querySelectorAll(".prism-collapsible-panel").forEach(function (section) {
576
+ var toggle = wireUp(section);
577
+ if (!toggle) return;
578
+
579
+ if (section === filtersSection) {
580
+ filtersToggle = toggle;
581
+ return;
582
+ }
583
+
584
+ otherEntries.push({ section: section, toggle: toggle });
585
+ });
586
+
587
+ if (filtersSection) {
588
+ document.body.classList.toggle("prism-filters-open", filtersSection.classList.contains("open"));
589
+ }
590
+
591
+ if (otherEntries.length > 0) buildOtherSectionsDropdown(otherEntries);
592
+
400
593
  // The active-filters bar (see lib/active_admin/views/active_filters_bar.rb,
401
594
  // ActiveAdminPrism::Configuration#active_filters_bar) is a shortcut
402
- // straight to the Filters form itself — clicking anywhere on it always
403
- // *opens* the panel (never toggles it closed; that's still the
404
- // funnel icon's job) and scrolls it into view, since #sidebar isn't
405
- // sticky the way the left nav is and can scroll out of view on a
406
- // long table.
595
+ // straight to the Filters form itself — clicking anywhere on it toggles
596
+ // the panel open/closed, same as the funnel icon, and scrolls it into
597
+ // view on open, since #sidebar isn't sticky the way the left nav is and
598
+ // can scroll out of view on a long table.
407
599
  var activeFiltersBar = document.getElementById("prism_active_filters_bar");
408
- if (activeFiltersBar) {
600
+ if (activeFiltersBar && filtersSection && filtersToggle) {
409
601
  activeFiltersBar.setAttribute("role", "button");
410
602
  activeFiltersBar.setAttribute("tabindex", "0");
411
- activeFiltersBar.setAttribute("aria-label", "Show filters");
603
+ activeFiltersBar.setAttribute("aria-label", "Toggle filters");
412
604
 
413
- var openFromBar = function (event) {
605
+ var toggleFromBar = function (event) {
414
606
  if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
415
607
  if (event.type === "keydown") event.preventDefault();
416
- setOpen(true);
417
- section.scrollIntoView({ behavior: "smooth", block: "nearest" });
608
+ var open = !filtersSection.classList.contains("open");
609
+ setOpen(filtersSection, filtersToggle, open);
610
+ if (open) filtersSection.scrollIntoView({ behavior: "smooth", block: "nearest" });
418
611
  };
419
612
 
420
- activeFiltersBar.addEventListener("click", openFromBar);
421
- activeFiltersBar.addEventListener("keydown", openFromBar);
613
+ activeFiltersBar.addEventListener("click", toggleFromBar);
614
+ activeFiltersBar.addEventListener("keydown", toggleFromBar);
422
615
  }
423
616
  });
424
617
  })();
@@ -500,3 +693,241 @@
500
693
  });
501
694
  });
502
695
  })();
696
+
697
+ // Consolidates a title bar with many `action_item` buttons (a resource
698
+ // that registers a dozen+ CSV upload / bulk-action links is a real
699
+ // example this was built for) into a single Select2 "jump menu" instead
700
+ // of a multi-row wall of individual buttons. See
701
+ // ActiveAdminPrism::Configuration#action_items_dropdown/
702
+ // #action_items_dropdown_threshold and lib/active_admin/views/title_bar.rb
703
+ // for the Ruby side (the "data-prism-action-items-threshold" attribute
704
+ // this reads — its mere presence also means #action_items_dropdown is
705
+ // on). Purely client-side DOM restructuring: each existing `.action_item`
706
+ // element is moved as-is (not a clone) into a hidden holding area, so
707
+ // whatever Ruby/Arbre content a host's own `action_item` block rendered —
708
+ // a plain link, a `button_to` form (its own data-method/data-confirm
709
+ // included), anything — keeps working completely unmodified; selecting an
710
+ // option here just finds and `.click()`s the real trigger inside it,
711
+ // rather than this file trying to reimplement navigation/submission
712
+ // itself. Select2 (vendor/select2/select2.full.min.js) is concatenated
713
+ // into this same compiled asset regardless of
714
+ // ActiveAdminPrism.configuration.select2 (that flag only controls
715
+ // auto-enhancing a *host's own* selects) — jQuery itself is always
716
+ // present too, since ActiveAdmin's own base.js already depends on it —
717
+ // so both are always available here with no extra host setup.
718
+ (function () {
719
+ "use strict";
720
+
721
+ document.addEventListener("DOMContentLoaded", function () {
722
+ var right = document.getElementById("titlebar_right");
723
+ if (!right || !right.dataset.prismActionItemsThreshold) return;
724
+ if (typeof jQuery === "undefined" || !jQuery.fn.select2) return;
725
+
726
+ // Not "!threshold" — 0 is this option's own default (see
727
+ // ActiveAdminPrism::Configuration#action_items_dropdown_threshold)
728
+ // and would be wrongly treated as falsy/absent otherwise.
729
+ var threshold = parseInt(right.dataset.prismActionItemsThreshold, 10);
730
+ var container = right.querySelector(":scope > .action_items");
731
+ if (isNaN(threshold) || !container) return;
732
+
733
+ // ":not(.prism-action-item-new)" — the "New <Resource>" button (see
734
+ // lib/active_admin/views/action_items.rb for where that class comes
735
+ // from) is excluded from consolidation and left inline in its
736
+ // original spot: it's the single most-used action on an index page,
737
+ // unlike the bulk-action/CSV-upload links this feature actually
738
+ // exists to tidy away, so sweeping it into a dropdown too would bury
739
+ // the one button visitors reach for constantly.
740
+ var items = Array.prototype.slice.call(
741
+ container.querySelectorAll(":scope > .action_item:not(.prism-action-item-new)")
742
+ );
743
+ if (items.length <= threshold) return;
744
+
745
+ var row = prismTitlebarActionsRow();
746
+ if (!row) return;
747
+
748
+ var wrapper = document.createElement("span");
749
+ wrapper.className = "prism-action-items-dropdown";
750
+
751
+ var holding = document.createElement("div");
752
+ holding.className = "prism-action-items-holding";
753
+ holding.hidden = true;
754
+
755
+ var select = document.createElement("select");
756
+ select.className = "prism-action-items-select";
757
+ // A real leading <option value=""> (kept selected, non-removable via
758
+ // Select2's "x") is what lets Select2 show a placeholder on a
759
+ // *single* select at all — see its own docs on this requirement.
760
+ select.appendChild(document.createElement("option"));
761
+
762
+ items.forEach(function (item, index) {
763
+ var trigger = item.querySelector("a, button, input[type=submit]");
764
+ var label = trigger ? (trigger.value || trigger.textContent) : item.textContent;
765
+
766
+ var option = document.createElement("option");
767
+ option.value = String(index);
768
+ option.textContent = (label || "").trim();
769
+ select.appendChild(option);
770
+
771
+ item.dataset.prismActionItemsIndex = String(index);
772
+ holding.appendChild(item);
773
+ });
774
+
775
+ wrapper.appendChild(select);
776
+ wrapper.appendChild(holding);
777
+ // Appended, not inserted first — this feature's DOMContentLoaded
778
+ // handler is registered after the "consolidated sidebar panels" one
779
+ // (prismTitlebarActionsRow's other caller), so its own "Sidebar Actions"
780
+ // dropdown (if any) has already claimed the left slot in the shared
781
+ // row by the time this runs; this one lands to its right.
782
+ row.appendChild(wrapper);
783
+
784
+ var $select = jQuery(select).select2({
785
+ width: "220px",
786
+ placeholder: "Actions (" + items.length + ")",
787
+ // See the matching comment in buildOtherSectionsDropdown above —
788
+ // always-visible search box, not gated behind a result-count
789
+ // threshold, is what makes this read as Select2 from the first item.
790
+ minimumResultsForSearch: 0
791
+ });
792
+
793
+ $select.on("select2:select", function (event) {
794
+ var index = event.params.data.id;
795
+ var item = holding.querySelector('[data-prism-action-items-index="' + index + '"]');
796
+ var trigger = item && item.querySelector("a, button, input[type=submit]");
797
+ if (trigger) trigger.click();
798
+ // Resets to the placeholder right away rather than showing the just-
799
+ // picked label — this is a jump menu (fires an action), not a
800
+ // persistent filter/setting, so there's nothing meaningful for it to
801
+ // stay "set" to.
802
+ $select.val("").trigger("change");
803
+ });
804
+ });
805
+ })();
806
+
807
+ // Aligns the shared title bar "jump menu" row (prismTitlebarActionsRow,
808
+ // above — the consolidated action_items dropdown and/or the consolidated
809
+ // "Sidebar Actions" sidebar-panels dropdown, whichever of those exist) with
810
+ // the index table's own right edge instead of #title_bar's: a table's
811
+ // columns don't necessarily stretch to fill the full content width, so
812
+ // those two edges often don't line up, and #title_bar itself spans wider
813
+ // than the table's own container to begin with (it sits above the
814
+ // #main_content/#sidebar split, not inside it). A separate, later IIFE
815
+ // rather than living inside either dropdown's own setup — either one, or
816
+ // both, might be the reason the row exists, and this only needs to run
817
+ // once regardless of which.
818
+ (function () {
819
+ "use strict";
820
+
821
+ document.addEventListener("DOMContentLoaded", function () {
822
+ var row = document.getElementById("prism-titlebar-actions-row");
823
+ var table = document.querySelector("#main_content table");
824
+ if (!row || !table) return;
825
+
826
+ // `transform: translateX` shifts the row left from wherever it
827
+ // already renders in normal flow — deliberately not `position:
828
+ // absolute/fixed`, which would need a containing block that actually
829
+ // shares the table's right edge, and none of this element's ancestors
830
+ // do. Staying in normal flow (just visually shifted) is also what
831
+ // keeps it aligned with the top of #title_bar for free, reading as a
832
+ // top-level menu rather than something floating at an offset.
833
+ function alignToTable() {
834
+ row.style.transform = "none";
835
+ var rowRect = row.getBoundingClientRect();
836
+ var tableRect = table.getBoundingClientRect();
837
+ if (!tableRect.width || !rowRect.width) return;
838
+
839
+ var shift = rowRect.right - tableRect.right;
840
+ row.style.transform = shift > 0 ? "translateX(-" + shift + "px)" : "none";
841
+ }
842
+
843
+ // Run once immediately, but layout isn't necessarily final yet at
844
+ // DOMContentLoaded — a scrollbar appearing/disappearing, webfonts, or
845
+ // anything else nudging the table's own width afterward all happen
846
+ // without the *window* itself ever resizing, so plain "resize"/"load"
847
+ // listeners can miss them (observed in practice: the table shrank by
848
+ // ~40px sometime after both had already fired). A ResizeObserver on
849
+ // the table itself sidesteps guessing which event actually catches
850
+ // it — it re-runs on any future width change, from any cause,
851
+ // including ones years from now this comment didn't anticipate.
852
+ alignToTable();
853
+
854
+ if (typeof ResizeObserver !== "undefined") {
855
+ new ResizeObserver(alignToTable).observe(table);
856
+ } else {
857
+ window.addEventListener("load", alignToTable);
858
+ var resizeTimer = null;
859
+ window.addEventListener("resize", function () {
860
+ clearTimeout(resizeTimer);
861
+ resizeTimer = setTimeout(alignToTable, 150);
862
+ });
863
+ }
864
+ });
865
+ })();
866
+
867
+ // Horizontal-scroll affordance for the index table. ActiveAdmin renders
868
+ // the table (or grid/block index) inside ".index_content"
869
+ // (lib/active_admin/views/pages/index.rb#render_index) — a sibling of
870
+ // the pagination/download-links footer (`#index_footer`), not an
871
+ // ancestor of it, both nested inside a shared outer container
872
+ // (".paginated_collection_contents"). Scoping the scroll to
873
+ // ".index_content" itself (scss-src/_tables.scss), rather than that
874
+ // outer container, is what keeps the footer in place instead of
875
+ // dragging it sideways out of view along with the table — it has no
876
+ // columns to align with anything anyway. A table simply cut off flush
877
+ // at ".index_content"'s edge otherwise looks identical whether there's
878
+ // one hidden column past it or ten, with nothing telling a visitor
879
+ // scrolling is even possible — so this also toggles two classes
880
+ // (".prism-scroll-left"/".prism-scroll-right") that scss-src/_tables.scss
881
+ // fades a shadow in/out on, on whichever edge(s) still have more to
882
+ // scroll toward, same "scroll shadow" pattern as Gmail/Trello's own
883
+ // tables. That part has to be class-toggling JS, not a pure-CSS trick,
884
+ // since it depends on the *current* scroll position, not just whether
885
+ // the element happens to be scrollable at all.
886
+ (function () {
887
+ "use strict";
888
+
889
+ function updateShadows(el) {
890
+ var maxScroll = el.scrollWidth - el.clientWidth;
891
+ // A 1px tolerance, not a bare 0/maxScroll comparison — fractional
892
+ // scrollLeft values (sub-pixel positions at some zoom levels/DPRs)
893
+ // mean it can land a hair short of either end, which would otherwise
894
+ // leave a shadow that never fully clears at that edge.
895
+ el.classList.toggle("prism-scroll-left", el.scrollLeft > 1);
896
+ el.classList.toggle("prism-scroll-right", el.scrollLeft < maxScroll - 1);
897
+ }
898
+
899
+ function wire(el) {
900
+ updateShadows(el);
901
+ el.addEventListener("scroll", function () {
902
+ updateShadows(el);
903
+ });
904
+
905
+ // Same reasoning as alignToTable's own ResizeObserver above: the
906
+ // table's width can settle after DOMContentLoaded (a scrollbar
907
+ // appearing, webfonts, a column's content changing after a filter)
908
+ // without the window itself ever resizing, so plain "resize"/"load"
909
+ // listeners can miss it. Observing its content, not just the
910
+ // container (whose own box size is unaffected by its scrollable
911
+ // content growing), is what actually catches that.
912
+ if (typeof ResizeObserver !== "undefined") {
913
+ var observer = new ResizeObserver(function () {
914
+ updateShadows(el);
915
+ });
916
+ observer.observe(el);
917
+ Array.prototype.forEach.call(el.children, function (child) {
918
+ observer.observe(child);
919
+ });
920
+ } else {
921
+ window.addEventListener("load", function () {
922
+ updateShadows(el);
923
+ });
924
+ window.addEventListener("resize", function () {
925
+ updateShadows(el);
926
+ });
927
+ }
928
+ }
929
+
930
+ document.addEventListener("DOMContentLoaded", function () {
931
+ document.querySelectorAll(".paginated_collection_contents > .index_content").forEach(wire);
932
+ });
933
+ })();
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Views
5
+ # Reopens ActiveAdmin::Views::ActionItems purely to mark the
6
+ # auto-registered "New <Resource>" action item with its own class —
7
+ # ActiveAdmin::Resource::ActionItems#add_default_new_action_item names
8
+ # it :new internally, but that name never reaches the rendered
9
+ # `span.action_item` itself (ActionItem#html_class only ever emits
10
+ # "action_item" plus a host's own optional :class, nothing derived
11
+ # from the name), so there'd otherwise be no way for CSS/JS to tell it
12
+ # apart from any other action_item at all.
13
+ #
14
+ # js-src/prism.js's consolidated-action-items dropdown reads this to
15
+ # always exclude the New-resource button from consolidation, leaving
16
+ # it inline in its original spot — it's the single most-used action on
17
+ # an index page, unlike the bulk-action/CSV-upload links this feature
18
+ # was actually built to tidy away, so sweeping it into a dropdown too
19
+ # would bury the one button visitors reach for constantly.
20
+ class ActionItems
21
+ def build(action_items)
22
+ action_items.each do |action_item|
23
+ classes = [action_item.html_class]
24
+ classes << "prism-action-item-new" if action_item.name == :new
25
+ span class: classes.join(" ") do
26
+ instance_exec(&action_item.block)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end