mirror of
https://github.com/wagtail/wagtail.git
synced 2024-11-25 05:02:57 +01:00
Add drilldown filters interface for universal listings
This commit is contained in:
parent
59479a49cc
commit
68df8a0ba3
86
client/scss/components/_drilldown.scss
Normal file
86
client/scss/components/_drilldown.scss
Normal file
@ -0,0 +1,86 @@
|
||||
.w-drilldown__contents {
|
||||
max-height: min(480px, 70vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.w-drilldown .w-drilldown__toggle {
|
||||
@apply w-label-1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-inline-start: theme('spacing.5');
|
||||
padding-inline-end: theme('spacing.5');
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: theme('colors.border-button-outline-hover');
|
||||
color: theme('colors.text-link-hover');
|
||||
cursor: pointer;
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border-color: Highlight;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
width: theme('spacing.5');
|
||||
height: theme('spacing.5');
|
||||
opacity: 1;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.w-drilldown__submenu {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
padding-inline-end: theme('spacing.[2.5]');
|
||||
}
|
||||
|
||||
.w-drilldown .w-drilldown__back {
|
||||
@apply w-label-1;
|
||||
position: relative;
|
||||
padding: theme('spacing.[2.5]');
|
||||
align-self: flex-start;
|
||||
|
||||
.icon {
|
||||
pointer-events: none;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: theme('colors.border-button-outline-hover');
|
||||
color: theme('colors.text-link-hover');
|
||||
cursor: pointer;
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border-color: Highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.w-drilldown__submenu .w-field__label {
|
||||
@apply w-label-1;
|
||||
margin-top: theme('spacing.2');
|
||||
margin-bottom: theme('spacing.3');
|
||||
}
|
||||
|
||||
.w-drilldown__count {
|
||||
$badge-size: theme('spacing.4');
|
||||
width: $badge-size;
|
||||
height: $badge-size;
|
||||
line-height: $badge-size;
|
||||
text-align: center;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: theme('fontWeight.bold');
|
||||
border-radius: theme('borderRadius.full');
|
||||
background-color: theme('colors.info.100');
|
||||
color: theme('colors.white.DEFAULT');
|
||||
|
||||
.w-filter-button & {
|
||||
position: absolute;
|
||||
top: calc(-0.5 * #{$badge-size});
|
||||
inset-inline-end: calc(-0.5 * #{$badge-size});
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@
|
||||
}
|
||||
|
||||
.w-filter-button {
|
||||
position: relative;
|
||||
width: theme('spacing.10');
|
||||
height: theme('spacing.10');
|
||||
padding: 0;
|
||||
|
@ -180,7 +180,7 @@
|
||||
.w-field__input {
|
||||
@apply w-mt-0;
|
||||
|
||||
input {
|
||||
> input {
|
||||
@apply w-text-16;
|
||||
min-height: theme('spacing.10');
|
||||
}
|
||||
|
@ -110,6 +110,7 @@ These are classes for components.
|
||||
@import 'components/panel';
|
||||
@import 'components/dialog';
|
||||
@import 'components/dismissible';
|
||||
@import 'components/drilldown';
|
||||
@import 'components/dropdown';
|
||||
@import 'components/dropdown-button';
|
||||
@import 'components/help-block';
|
||||
|
@ -31,6 +31,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme='drilldown'] {
|
||||
@apply w-rounded w-bg-surface-page w-shadow-md;
|
||||
width: 300px;
|
||||
|
||||
.tippy-content {
|
||||
@apply w-p-0;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme='dropdown-button'] {
|
||||
@apply w-rounded-none w-w-full w-bg-transparent;
|
||||
|
||||
|
123
client/src/controllers/DrilldownController.ts
Normal file
123
client/src/controllers/DrilldownController.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export class DrilldownController extends Controller<HTMLElement> {
|
||||
static targets = ['menu', 'toggle'];
|
||||
|
||||
static values = {
|
||||
activeSubmenu: { default: '', type: String },
|
||||
};
|
||||
|
||||
declare activeSubmenuValue: string;
|
||||
declare swapSuccessListener: (e: Event) => void;
|
||||
|
||||
declare readonly menuTarget: HTMLElement;
|
||||
declare readonly toggleTargets: HTMLButtonElement[];
|
||||
|
||||
connect() {
|
||||
this.updateToggleCounts();
|
||||
|
||||
this.swapSuccessListener = (e: Event) => {
|
||||
const swapEvent = e as CustomEvent<{ requestUrl: string }>;
|
||||
if ((e.target as HTMLElement)?.id === 'listing-results') {
|
||||
const params = new URLSearchParams(
|
||||
swapEvent.detail?.requestUrl.split('?')[1],
|
||||
);
|
||||
const filteredParams = new URLSearchParams();
|
||||
params.forEach((value, key) => {
|
||||
if (value.trim() !== '') {
|
||||
// Check if the value is not empty after trimming white space
|
||||
filteredParams.append(key, value);
|
||||
}
|
||||
});
|
||||
const queryString = '?' + filteredParams.toString();
|
||||
this.updateToggleCounts();
|
||||
window.history.replaceState(null, '', queryString);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('w-swap:success', this.swapSuccessListener);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener('w-swap:success', this.swapSuccessListener);
|
||||
}
|
||||
|
||||
open(e: MouseEvent) {
|
||||
const toggle = (e.target as HTMLElement)?.closest(
|
||||
'button',
|
||||
) as HTMLButtonElement;
|
||||
this.activeSubmenuValue = toggle.getAttribute('aria-controls') || '';
|
||||
}
|
||||
|
||||
close() {
|
||||
this.activeSubmenuValue = '';
|
||||
this.updateToggleCounts();
|
||||
}
|
||||
|
||||
activeSubmenuValueChanged(activeSubmenu: string, prevActiveSubmenu?: string) {
|
||||
if (prevActiveSubmenu) {
|
||||
const toggle = document.querySelector(
|
||||
`[aria-controls="${prevActiveSubmenu}"]`,
|
||||
) as HTMLButtonElement;
|
||||
this.toggle(false, toggle);
|
||||
}
|
||||
|
||||
if (activeSubmenu) {
|
||||
const toggle = document.querySelector(
|
||||
`[aria-controls="${activeSubmenu}"]`,
|
||||
) as HTMLButtonElement;
|
||||
this.toggle(true, toggle);
|
||||
}
|
||||
}
|
||||
|
||||
toggle(expanded: boolean, toggle: HTMLButtonElement) {
|
||||
const controls = toggle.getAttribute('aria-controls');
|
||||
const content = this.element.querySelector<HTMLElement>(`#${controls}`);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', expanded.toString());
|
||||
content.hidden = !expanded;
|
||||
this.menuTarget.hidden = expanded;
|
||||
this.element.classList.toggle('w-drilldown--active', expanded);
|
||||
|
||||
if (expanded) {
|
||||
content.focus();
|
||||
} else {
|
||||
toggle.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder function until this is more correctly set up with the backend.
|
||||
*/
|
||||
updateToggleCounts() {
|
||||
let sum = 0;
|
||||
|
||||
this.toggleTargets.forEach((toggle) => {
|
||||
const content = this.element.querySelector<HTMLElement>(
|
||||
`#${toggle.getAttribute('aria-controls')}`,
|
||||
);
|
||||
const counter = toggle.querySelector<HTMLElement>('.w-drilldown__count');
|
||||
if (!content || !counter) {
|
||||
return;
|
||||
}
|
||||
// Hack to detect fields with a non-default value.
|
||||
const nbActiveFields = content.querySelectorAll(
|
||||
'[type="checkbox"]:checked, [type="radio"]:checked:not([id$="_0"]), option:checked:not(:first-child), input:not([type="checkbox"], [type="radio"]):not(:placeholder-shown)',
|
||||
).length;
|
||||
counter.hidden = nbActiveFields === 0;
|
||||
counter.textContent = nbActiveFields.toString();
|
||||
sum += nbActiveFields;
|
||||
});
|
||||
|
||||
const sumCounter = this.element.querySelector<HTMLElement>(
|
||||
'.w-drilldown__count',
|
||||
);
|
||||
if (!sumCounter) {
|
||||
return;
|
||||
}
|
||||
sumCounter.hidden = sum === 0;
|
||||
sumCounter.textContent = sum.toString();
|
||||
}
|
||||
}
|
@ -86,6 +86,11 @@ const themeOptions = {
|
||||
maxWidth: 350,
|
||||
placement: 'bottom',
|
||||
},
|
||||
'drilldown': {
|
||||
arrow: false,
|
||||
maxWidth: 'none',
|
||||
placement: 'bottom-end',
|
||||
},
|
||||
'dropdown-button': {
|
||||
arrow: false,
|
||||
maxWidth: 'none',
|
||||
|
@ -9,6 +9,7 @@ import { CloneController } from './CloneController';
|
||||
import { CountController } from './CountController';
|
||||
import { DialogController } from './DialogController';
|
||||
import { DismissibleController } from './DismissibleController';
|
||||
import { DrilldownController } from './DrilldownController';
|
||||
import { DropdownController } from './DropdownController';
|
||||
import { InitController } from './InitController';
|
||||
import { OrderableController } from './OrderableController';
|
||||
@ -39,6 +40,7 @@ export const coreControllerDefinitions: Definition[] = [
|
||||
{ controllerConstructor: CountController, identifier: 'w-count' },
|
||||
{ controllerConstructor: DialogController, identifier: 'w-dialog' },
|
||||
{ controllerConstructor: DismissibleController, identifier: 'w-dismissible' },
|
||||
{ controllerConstructor: DrilldownController, identifier: 'w-drilldown' },
|
||||
{ controllerConstructor: DropdownController, identifier: 'w-dropdown' },
|
||||
{ controllerConstructor: InitController, identifier: 'w-init' },
|
||||
{ controllerConstructor: OrderableController, identifier: 'w-orderable' },
|
||||
|
@ -8,6 +8,7 @@
|
||||
- `attrs` (string?) - more attributes for parent element
|
||||
- `toggle_icon` (string?) - toggle icon identifier
|
||||
- `toggle_label` (string?) - Visible label for the toggle button
|
||||
- `toggle_suffix` (string?) - Visible content for the toggle button after the icon
|
||||
- `toggle_aria_label` (string?) - aria-label for the toggle button
|
||||
- `toggle_describedby` (string?) - aria-describedby for the toggle button
|
||||
- `toggle_classname` (string?) - additional toggle classes
|
||||
@ -24,6 +25,7 @@
|
||||
{% if toggle_icon %}
|
||||
{% icon name=toggle_icon classname="w-dropdown__toggle-icon" %}
|
||||
{% endif %}
|
||||
{{ toggle_suffix }}
|
||||
</button>
|
||||
|
||||
<div data-w-dropdown-target="content" class="w-dropdown__content" hidden>
|
||||
|
@ -79,14 +79,35 @@
|
||||
{% endif %}
|
||||
|
||||
{% if filters %}
|
||||
{% fragment as filters_icon %}{% icon name="sliders" title=_("Show filters") %}{% endfragment %}
|
||||
{% dialog_toggle classname="w-filter-button" dialog_id="filters-dialog" text=filters_icon %}
|
||||
|
||||
{% dialog theme="floating" id="filters-dialog" title=_("Filters") dialog_root_selector="[data-search-form]" %}
|
||||
{% for field in filters.form %}
|
||||
{% formattedfield field %}
|
||||
{% endfor %}
|
||||
{% enddialog %}
|
||||
<div class="w-drilldown" data-controller="w-drilldown">
|
||||
{% fragment as toggle_suffix %}
|
||||
<span class="w-drilldown__count" hidden></span>
|
||||
{% endfragment %}
|
||||
{% dropdown theme="drilldown" toggle_icon="sliders" toggle_classname="w-filter-button" toggle_aria_label=_("Show filters") toggle_suffix=toggle_suffix %}
|
||||
<div class="w-drilldown__contents">
|
||||
<div class="w-drilldown__menu" data-w-drilldown-target="menu">
|
||||
<h2 class="w-help-text w-pl-5 w-py-2.5 w-my-0">{% trans "Filter by" %}</h2>
|
||||
{% for field in filters.form %}
|
||||
<button class="w-drilldown__toggle" data-w-drilldown-target="toggle" data-action="click->w-drilldown#open" type="button" class="w-flex w-justify-between" aria-expanded="false" aria-controls="drilldown-{{field.auto_id}}">
|
||||
{{ field.label }}
|
||||
<div class="w-flex w-items-center w-gap-2">
|
||||
<span class="w-drilldown__count" hidden></span>
|
||||
{% icon name="arrow-right" %}
|
||||
</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for field in filters.form %}
|
||||
<div class="w-drilldown__submenu" id="drilldown-{{field.auto_id}}" hidden tabindex="-1">
|
||||
<button class="w-drilldown__back" type="button" data-action="click->w-drilldown#close" aria-label="{% trans 'Back' %}">
|
||||
{% icon name="arrow-left" %}
|
||||
</button>
|
||||
{% formattedfield field %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% enddropdown %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% comment %}
|
||||
|
Loading…
Reference in New Issue
Block a user