Просмотр исходного кода

Merge pull request #21969 from netbox-community/21924-improve-styling-and-consistency-of-floating-bulk-actions

Closes #21924: Refactor sticky bulk actions and form bars
bctiemann 1 месяц назад
Родитель
Сommit
e14f27ec83

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 82 - 29
netbox/project-static/src/buttons/floatBulk.ts

@@ -1,42 +1,95 @@
-import { getElements } from '../util';
+// Only target selection-driven sticky bars; 'always' bars are pure CSS
+const stickyActionsSelector = '.sticky-actions[data-sticky-position][data-sticky-when="selection"]';
+const selectionInputSelector = [
+  'input[type="checkbox"][name="pk"]',
+  'table tr th > input[type="checkbox"].toggle',
+  '#select-all',
+].join(', ');
+const checkedSelectionSelector = [
+  'input[type="checkbox"][name="pk"]:checked',
+  'table tr th > input[type="checkbox"].toggle:checked',
+  '#select-all:checked',
+].join(', ');
+const selectionControlSelector = [
+  '.bulk-action-buttons .btn',
+  '.bulk-action-buttons input:not([type="hidden"])',
+  '.bulk-action-buttons select',
+  '.bulk-action-buttons textarea',
+].join(', ');
+
+// Module-scoped guard: assumes this module is loaded exactly once per page.
+let listenersBound = false;
+
+/**
+ * Determine whether a sticky action group has an active selection in scope.
+ */
+function hasSelection(scope: ParentNode): boolean {
+  return scope.querySelector<HTMLInputElement>(checkedSelectionSelector) !== null;
+}
 
 /**
- * Conditionally add and remove a class that will float the button group
- * based on whether or not items in the list are checked
+ * Enable or disable controls that require a selection.
  */
-function toggleFloat(): void {
-  const checkedCheckboxes = document.querySelector<HTMLInputElement>(
-    'input[type="checkbox"][name="pk"]:checked',
-  );
-  const buttonGroup = document.querySelector<HTMLDivElement>(
-    'div.form.form-horizontal div.btn-list',
-  );
-  if (!buttonGroup) {
-    return;
+function setSelectionControlsDisabled(stickyActions: HTMLElement, disabled: boolean): void {
+  for (const control of stickyActions.querySelectorAll(selectionControlSelector)) {
+    if (
+      control instanceof HTMLButtonElement ||
+      control instanceof HTMLInputElement ||
+      control instanceof HTMLSelectElement ||
+      control instanceof HTMLTextAreaElement
+    ) {
+      control.disabled = disabled;
+    } else if (control instanceof HTMLAnchorElement) {
+      control.classList.toggle('disabled', disabled);
+      control.setAttribute('aria-disabled', String(disabled));
+
+      if (disabled) {
+        control.tabIndex = -1;
+      } else {
+        control.removeAttribute('tabindex');
+      }
+    }
   }
-  const isFloating = buttonGroup.classList.contains('btn-float-group-left');
-  if (checkedCheckboxes !== null && !isFloating) {
-    buttonGroup.classList.add('btn-float-group-left');
-  } else if (checkedCheckboxes === null && isFloating) {
-    buttonGroup.classList.remove('btn-float-group-left');
+}
+
+/**
+ * Update the state of a sticky action group.
+ */
+function updateStickyActions(stickyActions: HTMLElement): void {
+  const scope = stickyActions.closest('form') ?? document;
+  const isActive = hasSelection(scope);
+
+  stickyActions.classList.toggle('is-sticky-active', isActive);
+  setSelectionControlsDisabled(stickyActions, !isActive);
+}
+
+/**
+ * Update all sticky action groups on the page.
+ */
+function syncStickyActions(): void {
+  for (const stickyActions of document.querySelectorAll<HTMLElement>(stickyActionsSelector)) {
+    updateStickyActions(stickyActions);
   }
 }
 
 /**
- * Initialize floating bulk buttons.
+ * Initialize sticky action groups.
  */
 export function initFloatBulk(): void {
-  for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
-    element.addEventListener('change', () => {
-      toggleFloat();
-    });
-  }
-  // Handle the select-all checkbox
-  for (const element of getElements<HTMLInputElement>(
-    'table tr th > input[type="checkbox"].toggle',
-  )) {
-    element.addEventListener('change', () => {
-      toggleFloat();
+  if (!listenersBound) {
+    document.addEventListener('change', (event: Event) => {
+      const target = event.target;
+      if (target instanceof HTMLInputElement && target.matches(selectionInputSelector)) {
+        syncStickyActions();
+      }
     });
+
+    for (const eventName of ['htmx:afterSwap', 'htmx:oobAfterSwap']) {
+      document.body.addEventListener(eventName, syncStickyActions);
+    }
+
+    listenersBound = true;
   }
+
+  syncStickyActions();
 }

+ 84 - 22
netbox/project-static/styles/custom/_misc.scss

@@ -34,28 +34,6 @@ span.color-label {
   letter-spacing: .15rem;
 }
 
-// A floating div for form buttons
-.btn-float-group {
-  position: sticky;
-  bottom: 10px;
-  z-index: 4;
-}
-
-.btn-float-group-left {
-  @extend .btn-float-group;
-  float: left;  
-}
-
-.btn-float-group-right {
-  @extend .btn-float-group;
-  float: right;
-}
-
-// Override a transparent background
-.btn-float {
-  --tblr-btn-bg: var(--#{$prefix}bg-surface-tertiary) !important;
-}
-
 .logo {
   height: 80px;
 }
@@ -118,3 +96,87 @@ html[data-bs-theme=dark] {
     filter: grayscale(100%) invert(100%) brightness(80%);
   }
 }
+
+// ── Sticky action bars ──────────────────────────────────────
+// Base: shared by all sticky-action containers
+.sticky-actions {
+  z-index: 4;
+
+  &[data-sticky-when='always'],
+  &.is-sticky-active {
+    position: sticky;
+    bottom: map.get($spacers, 1);
+  }
+}
+
+// Inline (list / children) variants – selection-driven
+.sticky-actions[data-sticky-position='left'],
+.sticky-actions[data-sticky-position='right'] {
+  display: flex;
+  align-items: center;
+  gap: map.get($spacers, 2);
+  padding: map.get($spacers, 2);
+  max-width: 100%;
+  width: fit-content;
+}
+
+.sticky-actions[data-sticky-position='right'] {
+  margin-left: auto;
+}
+
+.sticky-actions[data-sticky-position='full'] {
+  max-width: 100%;
+}
+
+// Footer variant – edit forms (always visible, sticks to bottom: 0)
+// NOTE: requires `.sticky-actions` on the same element for `position: sticky`.
+.sticky-actions-footer {
+  max-width: 100%;
+  margin-top: map.get($spacers, 4);
+  padding: map.get($spacers, 3);
+  padding-bottom: calc(#{map.get($spacers, 3)} + env(safe-area-inset-bottom, 0));
+  background: var(--#{$prefix}bg-surface-secondary);
+  border-top: 1px solid var(--#{$prefix}border-color);
+
+  &[data-sticky-when='always'],
+  &.is-sticky-active {
+    bottom: 0;
+  }
+
+  > .btn-list {
+    justify-content: flex-end;
+    margin-bottom: 0;
+  }
+}
+
+.object-edit--with-sticky-actions {
+  padding-bottom: calc(#{map.get($spacers, 4)} + env(safe-area-inset-bottom, 0));
+}
+
+// Disabled-button styling inside selection-driven bars
+[data-sticky-when='selection'] .bulk-action-buttons .btn:disabled,
+[data-sticky-when='selection'] .bulk-action-buttons .btn.disabled {
+  border-color: transparent;
+  box-shadow: none;
+}
+
+[data-sticky-when='selection'] .bulk-action-buttons .btn.disabled {
+  pointer-events: none;
+}
+
+// Legacy aliases — remove in v4.7.0
+.btn-float-group-right {
+  position: sticky;
+  bottom: 10px;
+  z-index: 4;
+  margin-left: auto;
+  width: fit-content;
+}
+
+.btn-float-group-left {
+  position: sticky;
+  bottom: 10px;
+  z-index: 4;
+  width: fit-content;
+}
+// ── /Sticky action bars ─────────────────────────────────────

+ 7 - 4
netbox/templates/account/preferences.html

@@ -6,7 +6,7 @@
 {% block title %}{% trans "User Preferences" %}{% endblock %}
 
 {% block content %}
-  <form method="post" action="" class="object-edit" hx-disable="true" id="preferences-update">
+  <form method="post" action="" class="object-edit object-edit--with-sticky-actions" hx-disable="true" id="preferences-update">
     {% csrf_token %}
 
     {# Built-in preferences #}
@@ -73,9 +73,12 @@
         {% endif %}
       </div>
     </div>
-    <div class="text-end my-3">
-      <a href="{% url 'account:preferences' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
-      <button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
+
+    <div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
+      <div class="btn-list">
+        <a href="{% url 'account:preferences' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
+        <button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
+      </div>
     </div>
   </form>
 {% endblock %}

+ 5 - 3
netbox/templates/generic/bulk_edit.html

@@ -41,7 +41,7 @@ Context:
 
   {# Edit form #}
   <div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
-    <form action="" method="post" class="form form-horizontal mt-5">
+    <form action="" method="post" class="form form-horizontal object-edit--with-sticky-actions mt-5">
       <div id="form_fields" hx-disinherit="hx-select hx-swap">
         {% csrf_token %}
         {% if request.POST.return_url %}
@@ -123,9 +123,11 @@ Context:
           {% endif %}
           {% render_field form.background_job %}
         </div>
+      </div>{# /form_fields #}
 
-        <div class="btn-float-group-right">
-          <a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
+      <div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
+        <div class="btn-list">
+          <a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
           <button type="submit" name="_apply" class="btn btn-primary">{% trans "Apply" %}</button>
         </div>
       </div>

+ 4 - 2
netbox/templates/generic/object_children.html

@@ -33,9 +33,11 @@ Context:
                 {% include 'htmx/table.html' %}
             </div>
         </div>
-        <div class="d-print-none mt-2">
+        <div class="card btn-list sticky-actions d-print-none" data-sticky-position="right" data-sticky-when="selection">
             {% block bulk_controls %}
-              {% action_buttons actions model multi=True return_url=request.path %}
+              <div class="btn-list bulk-action-buttons">
+                {% action_buttons actions model multi=True return_url=request.path %}
+              </div>
               {% block bulk_extra_controls %}{% endblock %}
             {% endblock bulk_controls %}
         </div>

+ 20 - 18
netbox/templates/generic/object_edit.html

@@ -58,7 +58,7 @@ Context:
       {% include 'inc/missing_prerequisites.html' %}
     {% endif %}
 
-    <form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
+    <form action="" method="post" enctype="multipart/form-data" class="object-edit object-edit--with-sticky-actions mt-5">
       {% csrf_token %}
 
       {% block pre_form_fields %}{% endblock pre_form_fields %}
@@ -68,24 +68,26 @@ Context:
         {% endblock form %}
       </div>
 
-      <div class="btn-float-group-right">
-        {% block buttons %}
-          <a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
-          {% if object.pk %}
-            <button type="submit" name="_update" class="btn btn-primary">
-              {% trans "Save" %}
-            </button>
-          {% else %}
-            <div class="btn-group" role="group" aria-label="{% trans "Actions" %}">
-              <button type="submit" name="_create" class="btn btn-primary">
-                {% trans "Create" %}
+      <div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
+        <div class="btn-list">
+          {% block buttons %}
+            <a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
+            {% if object.pk %}
+              <button type="submit" name="_update" class="btn btn-primary">
+                {% trans "Save" %}
               </button>
-              <button type="submit" name="_addanother" class="btn btn-outline-primary btn-float">
-                {% trans "Create & Add Another" %}
-              </button>
-            </div>
-          {% endif %}
-        {% endblock buttons %}
+            {% else %}
+              <div class="btn-group" role="group" aria-label="{% trans "Actions" %}">
+                <button type="submit" name="_create" class="btn btn-primary">
+                  {% trans "Create" %}
+                </button>
+                <button type="submit" name="_addanother" class="btn btn-outline-primary">
+                  {% trans "Create & Add Another" %}
+                </button>
+              </div>
+            {% endif %}
+          {% endblock buttons %}
+        </div>
       </div>
     </form>
   </div>

+ 3 - 4
netbox/templates/generic/object_list.html

@@ -82,7 +82,7 @@ Context:
                     {% endblocktrans %}
                   </label>
                 </div>
-                <div class="bulk-action-buttons">
+                <div class="btn-list bulk-action-buttons">
                   {% action_buttons actions model multi=True %}
                 </div>
               </div>
@@ -91,7 +91,6 @@ Context:
         {% endif %}
 
         <div class="form form-horizontal">
-          {% csrf_token %}
           <input type="hidden" id="object-list-return-url" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
 
           {# Warn of any missing prerequisite objects #}
@@ -108,9 +107,9 @@ Context:
           {# /Objects table #}
 
           {# Form buttons #}
-          <div class="btn-list d-print-none">
+          <div class="card btn-list sticky-actions d-print-none" data-sticky-position="right" data-sticky-when="selection">
             {% block bulk_buttons %}
-              <div class="bulk-action-buttons">
+              <div class="btn-list bulk-action-buttons">
                 {% action_buttons actions model multi=True %}
               </div>
             {% endblock %}

+ 1 - 1
netbox/templates/htmx/quick_add.html

@@ -17,7 +17,7 @@
     {% csrf_token %}
     {% include 'htmx/form.html' %}
     <div class="text-end">
-      <button type="button" class="btn btn-outline-secondary btn-float" data-bs-dismiss="modal" aria-label="Cancel">
+      <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" aria-label="Cancel">
         {% trans "Cancel" %}
       </button>
       <button type="submit" name="_quickadd" class="btn btn-primary">

+ 1 - 1
netbox/templates/htmx/table.html

@@ -28,7 +28,7 @@
 
   {# Update the bulk action buttons with new query parameters #}
   {% if actions and not table.embedded %}
-    <div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
+    <div class="btn-list bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
       {% action_buttons actions model multi=True %}
     </div>
   {% endif %}

+ 9 - 7
netbox/templates/inc/filter_list.html

@@ -34,12 +34,14 @@
       {% endif %}
     </div>
   </div>
-  <div class="btn-float-group-right me-1">
-    <button type="button" class="btn btn-outline-danger btn-float" data-reset-select>
-      <i class="mdi mdi-backspace"></i> {% trans "Reset" %}
-    </button>
-    <button type="submit" class="btn btn-primary">
-      <i class="mdi mdi-magnify"></i> {% trans "Search" %}
-    </button>
+  <div class="sticky-actions sticky-actions-footer d-print-none" data-sticky-position="full" data-sticky-when="always">
+    <div class="btn-list">
+      <button type="button" class="btn btn-outline-danger" data-reset-select>
+        <i class="mdi mdi-backspace"></i> {% trans "Reset" %}
+      </button>
+      <button type="submit" class="btn btn-primary">
+        <i class="mdi mdi-magnify"></i> {% trans "Search" %}
+      </button>
+    </div>
   </div>
 </form>

Некоторые файлы не были показаны из-за большого количества измененных файлов