web: Add searchbar and enable it for "selected"
"Available" requires a round-trip to the provider level, so that's next.
This commit is contained in:
parent
0a0afbe08d
commit
b7e7835ce1
|
@ -62,6 +62,7 @@ const cssImportMapSources = [
|
||||||
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
|
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
|
||||||
'import PFTable from "@patternfly/patternfly/components/Table/table.css";',
|
'import PFTable from "@patternfly/patternfly/components/Table/table.css";',
|
||||||
'import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";',
|
'import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";',
|
||||||
|
'import PFTextInputGroup from "@patternfly/patternfly/components/TextInputGroup/text-input-group.css";',
|
||||||
'import PFTitle from "@patternfly/patternfly/components/Title/title.css";',
|
'import PFTitle from "@patternfly/patternfly/components/Title/title.css";',
|
||||||
'import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
|
'import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
|
||||||
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
|
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { PropertyValues, html, nothing } from "lit";
|
import { PropertyValues, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { createRef, ref } from "lit/directives/ref.js";
|
import { createRef, ref } from "lit/directives/ref.js";
|
||||||
import type { Ref } from "lit/directives/ref.js";
|
import type { Ref } from "lit/directives/ref.js";
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
|
@ -21,6 +21,7 @@ import "./components/ak-dual-select-controls";
|
||||||
import "./components/ak-dual-select-selected-pane";
|
import "./components/ak-dual-select-selected-pane";
|
||||||
import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane";
|
import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane";
|
||||||
import "./components/ak-pagination";
|
import "./components/ak-pagination";
|
||||||
|
import "./components/ak-search-bar";
|
||||||
import {
|
import {
|
||||||
EVENT_ADD_ALL,
|
EVENT_ADD_ALL,
|
||||||
EVENT_ADD_ONE,
|
EVENT_ADD_ONE,
|
||||||
|
@ -30,7 +31,7 @@ import {
|
||||||
EVENT_REMOVE_ONE,
|
EVENT_REMOVE_ONE,
|
||||||
EVENT_REMOVE_SELECTED,
|
EVENT_REMOVE_SELECTED,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { BasePagination, DualSelectPair } from "./types";
|
import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types";
|
||||||
|
|
||||||
function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) {
|
function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) {
|
||||||
const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2];
|
const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2];
|
||||||
|
@ -82,6 +83,9 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
@property({ attribute: "selected-label" })
|
@property({ attribute: "selected-label" })
|
||||||
selectedLabel = msg("Selected options");
|
selectedLabel = msg("Selected options");
|
||||||
|
|
||||||
|
@state()
|
||||||
|
selectedFilter: string = "";
|
||||||
|
|
||||||
availablePane: Ref<AkDualSelectAvailablePane> = createRef();
|
availablePane: Ref<AkDualSelectAvailablePane> = createRef();
|
||||||
|
|
||||||
selectedPane: Ref<AkDualSelectSelectedPane> = createRef();
|
selectedPane: Ref<AkDualSelectSelectedPane> = createRef();
|
||||||
|
@ -91,6 +95,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.handleMove = this.handleMove.bind(this);
|
this.handleMove = this.handleMove.bind(this);
|
||||||
|
this.handleSearch = this.handleSearch.bind(this);
|
||||||
[
|
[
|
||||||
EVENT_ADD_ALL,
|
EVENT_ADD_ALL,
|
||||||
EVENT_ADD_SELECTED,
|
EVENT_ADD_SELECTED,
|
||||||
|
@ -105,6 +110,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
this.addCustomListener("ak-dual-select-move", () => {
|
this.addCustomListener("ak-dual-select-move", () => {
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
});
|
});
|
||||||
|
this.addCustomListener("ak-search", this.handleSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
|
@ -221,6 +227,25 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
this.selectedPane.value!.clearMove();
|
this.selectedPane.value!.clearMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSearch(event: SearchbarEvent) {
|
||||||
|
switch (event.detail.source) {
|
||||||
|
case "ak-dual-list-available-search":
|
||||||
|
return this.handleAvailableSearch(event.detail.value);
|
||||||
|
case "ak-dual-list-selected-search":
|
||||||
|
return this.handleSelectedSearch(event.detail.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAvailbleSearch(value: string) {
|
||||||
|
console.log(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectedSearch(value: string) {
|
||||||
|
this.selectedFilter = value;
|
||||||
|
this.selectedPane.value!.clearMove();
|
||||||
|
}
|
||||||
|
|
||||||
get canAddAll() {
|
get canAddAll() {
|
||||||
// False unless any visible option cannot be found in the selected list, so can still be
|
// False unless any visible option cannot be found in the selected list, so can still be
|
||||||
// added.
|
// added.
|
||||||
|
@ -243,9 +268,18 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const selected = this.selectedFilter === "" ? this.selected :
|
||||||
|
this.selected.filter(([_k, v, s]) => {
|
||||||
|
const value = s !== undefined ? s : v;
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error("Filter only works when there's a string comparator");
|
||||||
|
}
|
||||||
|
return value.toLowerCase().includes(this.selectedFilter.toLowerCase())
|
||||||
|
});
|
||||||
|
|
||||||
const availableCount = this.availablePane.value?.toMove.size ?? 0;
|
const availableCount = this.availablePane.value?.toMove.size ?? 0;
|
||||||
const selectedCount = this.selectedPane.value?.toMove.size ?? 0;
|
const selectedCount = this.selectedPane.value?.toMove.size ?? 0;
|
||||||
const selectedTotal = this.selected.length;
|
const selectedTotal = selected.length;
|
||||||
const availableStatus =
|
const availableStatus =
|
||||||
availableCount > 0 ? msg(str`${availableCount} items marked to add.`) : " ";
|
availableCount > 0 ? msg(str`${availableCount} items marked to add.`) : " ";
|
||||||
const selectedTotalStatus = msg(str`${selectedTotal} items selected.`);
|
const selectedTotalStatus = msg(str`${selectedTotal} items selected.`);
|
||||||
|
@ -263,7 +297,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ak-search-bar name="ak-dual-list-available-search"></ak-search-bar>
|
||||||
<div class="pf-c-dual-list-selector__status">
|
<div class="pf-c-dual-list-selector__status">
|
||||||
<span
|
<span
|
||||||
class="pf-c-dual-list-selector__status-text"
|
class="pf-c-dual-list-selector__status-text"
|
||||||
|
@ -297,7 +331,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ak-search-bar name="ak-dual-list-selected-search"></ak-search-bar>
|
||||||
<div class="pf-c-dual-list-selector__status">
|
<div class="pf-c-dual-list-selector__status">
|
||||||
<span
|
<span
|
||||||
class="pf-c-dual-list-selector__status-text"
|
class="pf-c-dual-list-selector__status-text"
|
||||||
|
@ -308,7 +342,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
|
|
||||||
<ak-dual-select-selected-pane
|
<ak-dual-select-selected-pane
|
||||||
${ref(this.selectedPane)}
|
${ref(this.selectedPane)}
|
||||||
.selected=${this.selected.toSorted(alphaSort)}
|
.selected=${selected.toSorted(alphaSort)}
|
||||||
></ak-dual-select-selected-pane>
|
></ak-dual-select-selected-pane>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
65
web/src/elements/ak-dual-select/components/ak-search-bar.ts
Normal file
65
web/src/elements/ak-dual-select/components/ak-search-bar.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
|
import { html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { createRef, ref } from "lit/directives/ref.js";
|
||||||
|
import type { Ref } from "lit/directives/ref.js";
|
||||||
|
|
||||||
|
import { globalVariables, searchStyles } from "./search.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
import type { SearchbarEvent } from "../types";
|
||||||
|
|
||||||
|
const styles = [PFBase, globalVariables, searchStyles];
|
||||||
|
|
||||||
|
@customElement("ak-search-bar")
|
||||||
|
export class AkSearchbar extends CustomEmitterElement(AKElement) {
|
||||||
|
static get styles() {
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
input: Ref<HTMLInputElement> = createRef();
|
||||||
|
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
value = "";
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
name = "";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(_event: Event) {
|
||||||
|
if (this.input.value) {
|
||||||
|
this.value = this.input.value.value;
|
||||||
|
}
|
||||||
|
this.dispatchCustomEvent<SearchbarEvent>("ak-search", {
|
||||||
|
source: this.name,
|
||||||
|
value: this.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="pf-c-text-input-group">
|
||||||
|
<div class="pf-c-text-input-group__main pf-m-icon">
|
||||||
|
<span class="pf-c-text-input-group__text"
|
||||||
|
><span class="pf-c-text-input-group__icon"
|
||||||
|
><i class="fa fa-search fa-fw"></i></span
|
||||||
|
><input
|
||||||
|
type="search"
|
||||||
|
class="pf-c-text-input-group__text-input"
|
||||||
|
${ref(this.input)}
|
||||||
|
@input=${this.onChange}
|
||||||
|
value="${this.value}"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AkSearchbar;
|
166
web/src/elements/ak-dual-select/components/search.css.ts
Normal file
166
web/src/elements/ak-dual-select/components/search.css.ts
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import { css } from "lit";
|
||||||
|
|
||||||
|
// The `host` information for the Patternfly dual list selector came with some default settings that
|
||||||
|
// we do not want in a web component. By isolating what we *really* use into this collection here,
|
||||||
|
// we get all the benefits of Patternfly without having to wrestle without also having to counteract
|
||||||
|
// those default settings.
|
||||||
|
|
||||||
|
export const globalVariables = css`
|
||||||
|
:host {
|
||||||
|
--pf-c-text-input-group--BackgroundColor: var(--pf-global--BackgroundColor--100);
|
||||||
|
--pf-c-text-input-group__text--before--BorderWidth: var(--pf-global--BorderWidth--sm);
|
||||||
|
--pf-c-text-input-group__text--before--BorderColor: var(--pf-global--BorderColor--300);
|
||||||
|
--pf-c-text-input-group__text--after--BorderBottomWidth: var(--pf-global--BorderWidth--sm);
|
||||||
|
--pf-c-text-input-group__text--after--BorderBottomColor: var(--pf-global--BorderColor--200);
|
||||||
|
--pf-c-text-input-group--hover__text--after--BorderBottomColor: var(
|
||||||
|
--pf-global--primary-color--100
|
||||||
|
);
|
||||||
|
--pf-c-text-input-group__text--focus-within--after--BorderBottomWidth: var(
|
||||||
|
--pf-global--BorderWidth--md
|
||||||
|
);
|
||||||
|
--pf-c-text-input-group__text--focus-within--after--BorderBottomColor: var(
|
||||||
|
--pf-global--primary-color--100
|
||||||
|
);
|
||||||
|
--pf-c-text-input-group__main--first-child--not--text-input--MarginLeft: var(
|
||||||
|
--pf-global--spacer--sm
|
||||||
|
);
|
||||||
|
--pf-c-text-input-group__main--m-icon__text-input--PaddingLeft: var(
|
||||||
|
--pf-global--spacer--xl
|
||||||
|
);
|
||||||
|
--pf-c-text-input-group__main--RowGap: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group__main--ColumnGap: var(--pf-global--spacer--sm);
|
||||||
|
--pf-c-text-input-group--c-chip-group__main--PaddingTop: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group--c-chip-group__main--PaddingRight: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group--c-chip-group__main--PaddingBottom: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group__text-input--PaddingTop: var(--pf-global--spacer--form-element);
|
||||||
|
--pf-c-text-input-group__text-input--PaddingRight: var(--pf-global--spacer--sm);
|
||||||
|
--pf-c-text-input-group__text-input--PaddingBottom: var(--pf-global--spacer--form-element);
|
||||||
|
--pf-c-text-input-group__text-input--PaddingLeft: var(--pf-global--spacer--sm);
|
||||||
|
--pf-c-text-input-group__text-input--MinWidth: 12ch;
|
||||||
|
--pf-c-text-input-group__text-input--m-hint--Color: var(--pf-global--Color--dark-200);
|
||||||
|
--pf-c-text-input-group--placeholder--Color: var(--pf-global--Color--dark-200);
|
||||||
|
--pf-c-text-input-group__icon--Left: var(--pf-global--spacer--sm);
|
||||||
|
--pf-c-text-input-group__icon--Color: var(--pf-global--Color--200);
|
||||||
|
--pf-c-text-input-group__text--hover__icon--Color: var(--pf-global--Color--100);
|
||||||
|
--pf-c-text-input-group__icon--TranslateY: -50%;
|
||||||
|
--pf-c-text-input-group__utilities--MarginRight: var(--pf-global--spacer--sm);
|
||||||
|
--pf-c-text-input-group__utilities--MarginLeft: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group__utilities--child--MarginLeft: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group__utilities--c-button--PaddingRight: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group__utilities--c-button--PaddingLeft: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-text-input-group--m-disabled--Color: var(--pf-global--disabled-color--100);
|
||||||
|
--pf-c-text-input-group--m-disabled--BackgroundColor: var(--pf-global--disabled-color--300);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const searchStyles = css`
|
||||||
|
i.fa,
|
||||||
|
i.fas,
|
||||||
|
i.far,
|
||||||
|
i.fal,
|
||||||
|
i.fab {
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
display: inline-block;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-rendering: auto;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.fa-search:before {
|
||||||
|
content: "\f002";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa,
|
||||||
|
.fas {
|
||||||
|
position: relative;
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.fa-fw {
|
||||||
|
text-align: center;
|
||||||
|
width: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--pf-c-text-input-group--Color, inherit);
|
||||||
|
background-color: var(--pf-c-text-input-group--BackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--pf-c-text-input-group__main--RowGap)
|
||||||
|
var(--pf-c-text-input-group__main--ColumnGap);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__main.pf-m-icon {
|
||||||
|
--pf-c-text-input-group__text-input--PaddingLeft: var(
|
||||||
|
--pf-c-text-input-group__main--m-icon__text-input--PaddingLeft
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.pf-c-text-input-group__text {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-areas: "text-input";
|
||||||
|
flex: 1;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__text::before {
|
||||||
|
border: var(--pf-c-text-input-group__text--before--BorderWidth) solid
|
||||||
|
var(--pf-c-text-input-group__text--before--BorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__text::after {
|
||||||
|
border-bottom: var(--pf-c-text-input-group__text--after--BorderBottomWidth) solid
|
||||||
|
var(--pf-c-text-input-group__text--after--BorderBottomColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__text::before,
|
||||||
|
.pf-c-text-input-group__text::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__icon {
|
||||||
|
z-index: 4;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: var(--pf-c-text-input-group__icon--Left);
|
||||||
|
color: var(--pf-c-text-input-group__icon--Color);
|
||||||
|
transform: translateY(var(--pf-c-text-input-group__icon--TranslateY));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__text-input,
|
||||||
|
.pf-c-text-input-group__text-input.pf-m-hint {
|
||||||
|
grid-area: text-input;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pf-c-text-input-group__text-input {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-width: var(--pf-c-text-input-group__text-input--MinWidth);
|
||||||
|
padding: var(--pf-c-text-input-group__text-input--PaddingTop)
|
||||||
|
var(--pf-c-text-input-group__text-input--PaddingRight)
|
||||||
|
var(--pf-c-text-input-group__text-input--PaddingBottom)
|
||||||
|
var(--pf-c-text-input-group__text-input--PaddingLeft);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
`;
|
|
@ -86,8 +86,12 @@ export const globalVariables = css`
|
||||||
--pf-c-dual-list-selector__list-item--m-disabled__item-toggle-icon--Color: var(
|
--pf-c-dual-list-selector__list-item--m-disabled__item-toggle-icon--Color: var(
|
||||||
--pf-global--disabled-color--200
|
--pf-global--disabled-color--200
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Unique to authentik */
|
||||||
--pf-c-dual-list-selector--selection-desc--FontSize: var(--pf-global--FontSize--xs);
|
--pf-c-dual-list-selector--selection-desc--FontSize: var(--pf-global--FontSize--xs);
|
||||||
--pf-c-dual-list-selector--selection-desc--Color: var(--pf-global--Color--dark-200);
|
--pf-c-dual-list-selector--selection-desc--Color: var(--pf-global--Color--dark-200);
|
||||||
|
--pf-c-dual-list-selector__status--top-padding: var(--pf-global--spacer--xs);
|
||||||
|
--pf-c-dual-list-panels__gap: var(--pf-global--spacer--xs);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -104,6 +108,10 @@ export const mainStyles = css`
|
||||||
font-weight: var(--pf-c-dual-list-selector__title-text--FontWeight);
|
font-weight: var(--pf-c-dual-list-selector__title-text--FontWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pf-c-dual-list-selector__status {
|
||||||
|
padding-top: var(--pf-c-dual-list-selector__status--top-padding);
|
||||||
|
}
|
||||||
|
|
||||||
.pf-c-dual-list-selector__status-text {
|
.pf-c-dual-list-selector__status-text {
|
||||||
font-size: var(--pf-c-dual-list-selector__status-text--FontSize);
|
font-size: var(--pf-c-dual-list-selector__status-text--FontSize);
|
||||||
color: var(--pf-c-dual-list-selector__status-text--Color);
|
color: var(--pf-c-dual-list-selector__status-text--Color);
|
||||||
|
@ -117,7 +125,8 @@ export const mainStyles = css`
|
||||||
.ak-available-pane,
|
.ak-available-pane,
|
||||||
.ak-selected-pane {
|
.ak-selected-pane {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto 1fr auto;
|
grid-template-rows: auto auto auto 1fr auto;
|
||||||
|
gap: var(--pf-c-dual-list-panels__gap);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
|
import { debounce } from "@goauthentik/elements/utils/debounce";
|
||||||
|
import { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
|
import { TemplateResult, html } from "lit";
|
||||||
|
|
||||||
|
import "../components/ak-search-bar";
|
||||||
|
import { AkSearchbar } from "../components/ak-search-bar";
|
||||||
|
|
||||||
|
const metadata: Meta<AkSearchbar> = {
|
||||||
|
title: "Elements / Dual Select / Search Bar",
|
||||||
|
component: "ak-dual-select-search",
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: "A search input bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default metadata;
|
||||||
|
|
||||||
|
const container = (testItem: TemplateResult) =>
|
||||||
|
html` <div style="background: #fff; padding: 2em">
|
||||||
|
<style>
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
${testItem}
|
||||||
|
<p>Messages received from the button:</p>
|
||||||
|
<div id="action-button-message-pad" style="margin-top: 1em"></div>
|
||||||
|
<div id="action-button-message-pad-2" style="margin-top: 1em"></div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const displayMessage = (result: any) => {
|
||||||
|
const doc = new DOMParser().parseFromString(`<p><i>Content</i>: ${result}</p>`, "text/xml");
|
||||||
|
const target = document.querySelector("#action-button-message-pad");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
target!.replaceChildren(doc.firstChild!);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const displayMessage2 = (result: any) => {
|
||||||
|
console.log("Huh.");
|
||||||
|
const doc = new DOMParser().parseFromString(`<p><i>Behavior</i>: ${result}</p>`, "text/xml");
|
||||||
|
const target = document.querySelector("#action-button-message-pad-2");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
target!.replaceChildren(doc.firstChild!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayMessage2b = debounce(displayMessage2, 250);
|
||||||
|
|
||||||
|
window.addEventListener("input", (event: Event) => {
|
||||||
|
const message = (event.target as HTMLInputElement | undefined)?.value ?? "-- undefined --";
|
||||||
|
displayMessage(message);
|
||||||
|
displayMessage2b(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
type Story = StoryObj;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => container(html` <ak-search-bar></ak-search-bar>`),
|
||||||
|
};
|
|
@ -17,3 +17,10 @@ export type DataProvision = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataProvider = (page: number) => Promise<DataProvision>;
|
export type DataProvider = (page: number) => Promise<DataProvision>;
|
||||||
|
|
||||||
|
export interface SearchbarEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
source: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
13
web/src/elements/utils/debounce.ts
Normal file
13
web/src/elements/utils/debounce.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type Callback = (...args: any[]) => any;
|
||||||
|
export function debounce<F extends Callback, T extends object>(callback: F, wait: number) {
|
||||||
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
return (...args: Parameters<F>) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const context: T = this satisfies object;
|
||||||
|
if (timeout !== undefined) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => callback.apply(context, args), wait);
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,20 +10,25 @@ export const isCustomEvent = (v: any): v is CustomEvent =>
|
||||||
export function CustomEmitterElement<T extends Constructor<LitElement>>(superclass: T) {
|
export function CustomEmitterElement<T extends Constructor<LitElement>>(superclass: T) {
|
||||||
return class EmmiterElementHandler extends superclass {
|
return class EmmiterElementHandler extends superclass {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
dispatchCustomEvent(eventName: string, detail: any = {}, options = {}) {
|
dispatchCustomEvent<F extends CustomEvent>(
|
||||||
|
eventName: string,
|
||||||
|
detail: any = {},
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
const fullDetail =
|
const fullDetail =
|
||||||
typeof detail === "object" && !Array.isArray(detail)
|
typeof detail === "object" && !Array.isArray(detail)
|
||||||
? {
|
? {
|
||||||
...detail,
|
...detail,
|
||||||
}
|
}
|
||||||
: detail;
|
: detail;
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(eventName, {
|
new CustomEvent(eventName, {
|
||||||
composed: true,
|
composed: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
...options,
|
...options,
|
||||||
detail: fullDetail,
|
detail: fullDetail,
|
||||||
}),
|
}) as F,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Reference in a new issue