active_admin_prism 0.1.2 → 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 +159 -0
- data/INTEGRATION.md +274 -35
- data/README.md +5 -1
- data/app/assets/javascripts/active_admin_prism/prism.js +1 -1
- data/app/assets/stylesheets/active_admin_prism/prism.css +1 -1
- data/app/views/layouts/active_admin_logged_out.html.erb +76 -0
- data/js-src/prism.js +565 -27
- data/lib/active_admin/views/action_items.rb +32 -0
- data/lib/active_admin/views/active_filters_bar.rb +94 -0
- data/lib/active_admin/views/filters_sidebar.rb +42 -13
- data/lib/active_admin/views/flash_messages.rb +40 -22
- data/lib/active_admin/views/prism_sidebar.rb +39 -6
- data/lib/active_admin/views/title_bar.rb +27 -0
- data/lib/active_admin_prism/configuration.rb +80 -0
- data/lib/active_admin_prism/version.rb +1 -1
- data/lib/active_admin_prism.rb +3 -0
- data/lib/prism_icons.rb +47 -1
- data/scss-src/_base.scss +161 -24
- data/scss-src/_buttons.scss +70 -0
- data/scss-src/_forms.scss +15 -0
- data/scss-src/_login.scss +5 -4
- data/scss-src/_panels.scss +189 -14
- data/scss-src/_select2.scss +85 -12
- data/scss-src/_sidebar.scss +145 -4
- data/scss-src/_tables.scss +102 -4
- data/scss-src/_variables.scss +3 -0
- metadata +7 -6
data/js-src/prism.js
CHANGED
|
@@ -75,6 +75,73 @@
|
|
|
75
75
|
});
|
|
76
76
|
})();
|
|
77
77
|
|
|
78
|
+
// Desktop sidebar rail-collapse toggle (ActiveAdminPrism::Configuration
|
|
79
|
+
// #sidebar_collapsible) — separate from the mobile off-canvas toggle above:
|
|
80
|
+
// that one slides the whole sidebar on/off screen below 900px; this one
|
|
81
|
+
// shrinks it to a narrow icon-only rail on desktop instead, and the two
|
|
82
|
+
// don't interact (scss-src/_sidebar.scss scopes rail-collapse to nothing in
|
|
83
|
+
// particular, but the mobile media query's own `transform`/`position: fixed`
|
|
84
|
+
// rules take over below 900px regardless of this class). State persists to
|
|
85
|
+
// localStorage under its own key, same convention as the nav-group state
|
|
86
|
+
// above, so a returning visitor keeps their last choice.
|
|
87
|
+
(function () {
|
|
88
|
+
"use strict";
|
|
89
|
+
|
|
90
|
+
var STORAGE_KEY = "prism_sidebar_collapsed";
|
|
91
|
+
|
|
92
|
+
function setCollapsed(toggle, collapsed) {
|
|
93
|
+
document.body.classList.toggle("prism-sidebar-collapsed", collapsed);
|
|
94
|
+
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
95
|
+
toggle.setAttribute("aria-label", collapsed ? "Expand sidebar" : "Collapse sidebar");
|
|
96
|
+
try {
|
|
97
|
+
window.localStorage.setItem(STORAGE_KEY, collapsed ? "1" : "0");
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// localStorage unavailable — degrade to in-memory only, no
|
|
100
|
+
// persistence across reloads.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
105
|
+
var toggle = document.querySelector("[data-prism-toggle-sidebar-collapse]");
|
|
106
|
+
if (!toggle) return;
|
|
107
|
+
|
|
108
|
+
var stored;
|
|
109
|
+
try {
|
|
110
|
+
stored = window.localStorage.getItem(STORAGE_KEY);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
stored = null;
|
|
113
|
+
}
|
|
114
|
+
if (stored === "1") setCollapsed(toggle, true);
|
|
115
|
+
|
|
116
|
+
toggle.addEventListener("click", function () {
|
|
117
|
+
setCollapsed(toggle, !document.body.classList.contains("prism-sidebar-collapsed"));
|
|
118
|
+
});
|
|
119
|
+
toggle.addEventListener("keydown", function (event) {
|
|
120
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
setCollapsed(toggle, !document.body.classList.contains("prism-sidebar-collapsed"));
|
|
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
|
+
});
|
|
142
|
+
});
|
|
143
|
+
})();
|
|
144
|
+
|
|
78
145
|
// Sidebar menu search box (lib/active_admin/views/prism_sidebar.rb
|
|
79
146
|
// #render_search_box) — filters the "Pages" nav as the visitor types,
|
|
80
147
|
// matching an item's own label at ANY nesting depth, not just top-level
|
|
@@ -222,7 +289,9 @@
|
|
|
222
289
|
});
|
|
223
290
|
})();
|
|
224
291
|
|
|
225
|
-
// Flash messages: dismiss button + auto-hide
|
|
292
|
+
// Flash messages: dismiss button + auto-hide, toast-style (see
|
|
293
|
+
// scss-src/_base.scss for the floating card layout and the
|
|
294
|
+
// ".prism-flash-progress" countdown bar this keeps in sync with). See
|
|
226
295
|
// lib/active_admin/views/flash_messages.rb, which renders the
|
|
227
296
|
// ".prism-flash-dismiss" button markup this responds to, and two data
|
|
228
297
|
// attributes this reads at runtime so Ruby-side config controls behavior
|
|
@@ -249,6 +318,36 @@
|
|
|
249
318
|
}, transitionMs);
|
|
250
319
|
}
|
|
251
320
|
|
|
321
|
+
// Hovering a toast pauses both its countdown bar (CSS
|
|
322
|
+
// "animation-play-state: paused" on :hover, in scss-src/_base.scss) and
|
|
323
|
+
// the actual removal timer here, tracking whatever time was left rather
|
|
324
|
+
// than restarting from the full duration — a flash a visitor is
|
|
325
|
+
// mid-read shouldn't silently vanish out from under them just because
|
|
326
|
+
// its timer happened to elapse while their mouse was still over it.
|
|
327
|
+
function armAutoDismiss(flash, totalMs, transitionMs) {
|
|
328
|
+
var remainingMs = totalMs;
|
|
329
|
+
var timer = null;
|
|
330
|
+
var startedAt = null;
|
|
331
|
+
|
|
332
|
+
function start() {
|
|
333
|
+
startedAt = Date.now();
|
|
334
|
+
timer = setTimeout(function () {
|
|
335
|
+
dismiss(flash, transitionMs);
|
|
336
|
+
}, remainingMs);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function pause() {
|
|
340
|
+
if (!timer) return;
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
timer = null;
|
|
343
|
+
remainingMs -= Date.now() - startedAt;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
flash.addEventListener("mouseenter", pause);
|
|
347
|
+
flash.addEventListener("mouseleave", start);
|
|
348
|
+
start();
|
|
349
|
+
}
|
|
350
|
+
|
|
252
351
|
document.addEventListener("DOMContentLoaded", function () {
|
|
253
352
|
var flashesWrapper = document.querySelector(".flashes");
|
|
254
353
|
var flashes = document.querySelectorAll(".flashes .flash");
|
|
@@ -259,9 +358,7 @@
|
|
|
259
358
|
|
|
260
359
|
if (autoDismissMs) {
|
|
261
360
|
flashes.forEach(function (flash) {
|
|
262
|
-
|
|
263
|
-
dismiss(flash, transitionMs);
|
|
264
|
-
}, autoDismissMs);
|
|
361
|
+
armAutoDismiss(flash, autoDismissMs, transitionMs);
|
|
265
362
|
});
|
|
266
363
|
}
|
|
267
364
|
|
|
@@ -273,46 +370,249 @@
|
|
|
273
370
|
});
|
|
274
371
|
})();
|
|
275
372
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
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
|
|
280
425
|
// ActiveAdminPrism.configuration.collapsible_filters is false (the
|
|
281
426
|
// "prism-filters-collapsible-disabled" body class, set in
|
|
282
427
|
// lib/active_admin/views/flash_messages.rb#body_classes).
|
|
283
428
|
(function () {
|
|
284
429
|
"use strict";
|
|
285
430
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}
|
|
291
446
|
|
|
447
|
+
function wireUp(section) {
|
|
292
448
|
var toggle = section.querySelector(":scope > h3");
|
|
293
|
-
if (!toggle) return;
|
|
449
|
+
if (!toggle) return null;
|
|
294
450
|
|
|
295
451
|
toggle.setAttribute("role", "button");
|
|
296
452
|
toggle.setAttribute("tabindex", "0");
|
|
297
453
|
toggle.setAttribute("aria-expanded", section.classList.contains("open") ? "true" : "false");
|
|
298
|
-
// Mirrored onto <body> because #main_content (the table/grid area)
|
|
299
|
-
// is a *sibling* of #sidebar, not a descendant of this section — CSS
|
|
300
|
-
// can't reach across siblings, so scss-src/_panels.scss keys the
|
|
301
|
-
// #main_content/#sidebar width reflow off this body class instead.
|
|
302
|
-
document.body.classList.toggle("prism-filters-open", section.classList.contains("open"));
|
|
303
454
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
document.body.classList.toggle("prism-filters-open", open);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
toggle.addEventListener("click", toggleOpen);
|
|
455
|
+
toggle.addEventListener("click", function () {
|
|
456
|
+
setOpen(section, toggle, !section.classList.contains("open"));
|
|
457
|
+
});
|
|
311
458
|
toggle.addEventListener("keydown", function (event) {
|
|
312
459
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
313
460
|
event.preventDefault();
|
|
314
|
-
|
|
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();
|
|
518
|
+
});
|
|
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 });
|
|
315
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
|
+
|
|
593
|
+
// The active-filters bar (see lib/active_admin/views/active_filters_bar.rb,
|
|
594
|
+
// ActiveAdminPrism::Configuration#active_filters_bar) is a shortcut
|
|
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.
|
|
599
|
+
var activeFiltersBar = document.getElementById("prism_active_filters_bar");
|
|
600
|
+
if (activeFiltersBar && filtersSection && filtersToggle) {
|
|
601
|
+
activeFiltersBar.setAttribute("role", "button");
|
|
602
|
+
activeFiltersBar.setAttribute("tabindex", "0");
|
|
603
|
+
activeFiltersBar.setAttribute("aria-label", "Toggle filters");
|
|
604
|
+
|
|
605
|
+
var toggleFromBar = function (event) {
|
|
606
|
+
if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
|
|
607
|
+
if (event.type === "keydown") event.preventDefault();
|
|
608
|
+
var open = !filtersSection.classList.contains("open");
|
|
609
|
+
setOpen(filtersSection, filtersToggle, open);
|
|
610
|
+
if (open) filtersSection.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
activeFiltersBar.addEventListener("click", toggleFromBar);
|
|
614
|
+
activeFiltersBar.addEventListener("keydown", toggleFromBar);
|
|
615
|
+
}
|
|
316
616
|
});
|
|
317
617
|
})();
|
|
318
618
|
|
|
@@ -393,3 +693,241 @@
|
|
|
393
693
|
});
|
|
394
694
|
});
|
|
395
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
|
+
})();
|