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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/INTEGRATION.md +147 -22
- data/README.md +3 -1
- data/app/assets/javascripts/active_admin_prism/prism.js +1 -1
- data/app/assets/stylesheets/active_admin_prism/prism.css +1 -1
- data/js-src/prism.js +470 -39
- data/lib/active_admin/views/action_items.rb +32 -0
- data/lib/active_admin/views/filters_sidebar.rb +42 -13
- data/lib/active_admin/views/prism_sidebar.rb +4 -3
- data/lib/active_admin/views/title_bar.rb +27 -0
- data/lib/active_admin_prism/configuration.rb +40 -0
- data/lib/active_admin_prism/version.rb +1 -1
- data/lib/active_admin_prism.rb +2 -0
- data/lib/prism_icons.rb +21 -0
- data/scss-src/_buttons.scss +70 -0
- data/scss-src/_panels.scss +28 -14
- data/scss-src/_select2.scss +56 -0
- data/scss-src/_tables.scss +102 -4
- metadata +3 -1
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
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
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", "
|
|
603
|
+
activeFiltersBar.setAttribute("aria-label", "Toggle filters");
|
|
412
604
|
|
|
413
|
-
var
|
|
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
|
-
|
|
417
|
-
|
|
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",
|
|
421
|
-
activeFiltersBar.addEventListener("keydown",
|
|
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
|