
* web: Packagify live reload plugin. * web: Use shared formatter. * web: Format. * web: Use project mode typecheck. * web: Fix type errors.
234 lines
7.8 KiB
TypeScript
234 lines
7.8 KiB
TypeScript
import { AkControlElement } from "@goauthentik/elements/AkControlElement";
|
|
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
|
|
|
import { msg } from "@lit/localize";
|
|
import { PropertyValues } from "@lit/reactive-element";
|
|
import { TemplateResult, css, html } from "lit";
|
|
import { customElement, property, queryAll, state } from "lit/decorators.js";
|
|
import { map } from "lit/directives/map.js";
|
|
|
|
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
|
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|
|
|
type CheckboxKv = { name: string; label: string | TemplateResult };
|
|
type CheckboxPr = [string, string | TemplateResult];
|
|
export type CheckboxPair = CheckboxKv | CheckboxPr;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const isCheckboxPr = (t: any): t is CheckboxPr => Array.isArray(t);
|
|
function* kvToPairs(items: CheckboxPair[]): Iterable<CheckboxPr> {
|
|
for (const item of items) {
|
|
yield isCheckboxPr(item) ? item : [item.name, item.label];
|
|
}
|
|
}
|
|
|
|
const AkElementWithCustomEvents = CustomEmitterElement(AkControlElement);
|
|
|
|
/**
|
|
* @element ak-checkbox-group
|
|
*
|
|
* @class CheckboxGroup
|
|
*
|
|
* @description
|
|
* CheckboxGroup renders a collection of checkboxes in a linear list. Multiple
|
|
* checkboxes may be picked.
|
|
*
|
|
* @attr {options} - An array of either `[string, string | TemplateResult]` or
|
|
* `{ name: string, label: string | TemplateResult }`. The first value or
|
|
* `name` field must be a valid HTML identifier compatible with the HTML
|
|
* `name` attribute.
|
|
*
|
|
* @attr {value} - An array of `name` values corresponding to the options that
|
|
* are selected when the element is rendered.
|
|
*
|
|
* @attr {name} - The name of this element as it will appear in any <form>
|
|
* transaction
|
|
*
|
|
* @attr {required} - If true, and if name is set, and no values are chosen,
|
|
* will automatically fail a form `submit` event, providing a warning
|
|
* message for any labeling. Note: if `name` is not set, this has no effect,
|
|
* and a warn() will appear on the console.
|
|
*
|
|
* @event {input} - Fired when the component's value has changed. Current value
|
|
* as an array of `name` will be in the `Event.detail` field.
|
|
*
|
|
* @event {change} - Fired when the component's value has changed. Current value
|
|
* as an array of `name` will be in the `Event.detail` field.
|
|
*
|
|
* @csspart checkbox - The div containing the checkbox item and the label
|
|
* @csspart label - the label
|
|
* @csspart input - the input item
|
|
* @csspart checkbox-group - the wrapper div with flexbox control
|
|
*
|
|
* ## Bigger hit area
|
|
*
|
|
* Providing properly formatted names for selections allows the element to
|
|
* associate the label with the event, so the entire horizontal area from
|
|
* checkbox to end-of-label will be the hit area.
|
|
*
|
|
* ## FormAssociated compliance
|
|
*
|
|
* If a <form> component is a parent, this component will correctly send its
|
|
* values to the form for `x-form-encoded` data; multiples will appear in the
|
|
* form of `name=value1&name=value2` format, and must be unpacked into an array
|
|
* correctly on the server side according to the CGI (common gateway interface)
|
|
* protocol.
|
|
*
|
|
*/
|
|
|
|
@customElement("ak-checkbox-group")
|
|
export class CheckboxGroup extends AkElementWithCustomEvents {
|
|
static get styles() {
|
|
return [
|
|
PFBase,
|
|
PFForm,
|
|
PFCheck,
|
|
css`
|
|
.pf-c-form__group-control {
|
|
padding-top: calc(
|
|
var(--pf-c-form--m-horizontal__group-label--md--PaddingTop) * 1.3
|
|
);
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
static get formAssociated() {
|
|
return true;
|
|
}
|
|
|
|
@property({ type: Array })
|
|
options: CheckboxPair[] = [];
|
|
|
|
@property({ type: Array })
|
|
value: string[] = [];
|
|
|
|
@property({ type: String })
|
|
name?: string;
|
|
|
|
@property({ type: Boolean })
|
|
required = false;
|
|
|
|
@queryAll('input[type="checkbox"]')
|
|
checkboxes!: NodeListOf<HTMLInputElement>;
|
|
|
|
@state()
|
|
values: string[] = [];
|
|
|
|
internals?: ElementInternals;
|
|
doneFirstUpdate = false;
|
|
|
|
json() {
|
|
return this.values;
|
|
}
|
|
|
|
private get formValue() {
|
|
if (this.name === undefined) {
|
|
throw new Error("This cannot be called without having the name set.");
|
|
}
|
|
const name = this.name;
|
|
const entries = new FormData();
|
|
this.values.forEach((v) => entries.append(name, v));
|
|
return entries;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.onClick = this.onClick.bind(this);
|
|
}
|
|
|
|
onClick(ev: Event) {
|
|
ev.stopPropagation();
|
|
this.values = Array.from(this.checkboxes)
|
|
.filter((checkbox) => checkbox.checked)
|
|
.map((checkbox) => checkbox.name);
|
|
this.dispatchCustomEvent("change", this.values);
|
|
this.dispatchCustomEvent("input", this.values);
|
|
if (this.internals) {
|
|
this.internals.setValidity({});
|
|
if (this.required && this.values.length === 0) {
|
|
this.internals.setValidity(
|
|
{
|
|
valueMissing: true,
|
|
},
|
|
msg("A selection is required"),
|
|
this,
|
|
);
|
|
}
|
|
this.internals.setFormValue(this.formValue);
|
|
}
|
|
// Doing a write-back so anyone examining the checkbox.value field will get something
|
|
// meaningful. Doesn't do anything for anyone, usually, but it's nice to have.
|
|
this.value = this.values;
|
|
}
|
|
|
|
willUpdate(changed: PropertyValues<this>) {
|
|
if (changed.has("value") && !this.doneFirstUpdate) {
|
|
this.doneFirstUpdate = true;
|
|
this.values = this.value;
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.dataset.akControl = "true";
|
|
if (this.name && !this.internals) {
|
|
this.internals = this.attachInternals();
|
|
}
|
|
if (this.internals && this.name) {
|
|
this.internals.ariaRequired = this.required ? "true" : "false";
|
|
}
|
|
if (this.required && !this.internals) {
|
|
console.warn(
|
|
"Setting `required` on ak-checkbox-group has no effect when the `name` attribute is unset",
|
|
);
|
|
}
|
|
// These are necessary to prevent the input components' own events from
|
|
// leaking out. This helps maintain the illusion that this component
|
|
// behaves similarly to the multiple selection behavior of, well,
|
|
// <select multiple>.
|
|
this.addEventListener("input", (ev) => {
|
|
ev.stopPropagation();
|
|
});
|
|
this.addEventListener("change", (ev) => {
|
|
ev.stopPropagation();
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const renderOne = ([name, label]: CheckboxPr) => {
|
|
const selected = this.values.includes(name);
|
|
const blockFwd = (e: Event) => {
|
|
e.stopImmediatePropagation();
|
|
};
|
|
|
|
return html` <div part="checkbox" class="pf-c-check" @click=${this.onClick}>
|
|
<input
|
|
part="input"
|
|
@change=${blockFwd}
|
|
@input=${blockFwd}
|
|
name="${name}"
|
|
class="pf-c-check__input"
|
|
type="checkbox"
|
|
?checked=${selected}
|
|
id="ak-check-${name}"
|
|
/>
|
|
<label part="label" class="pf-c-check__label" for="ak-check-${name}"
|
|
>${label}</label
|
|
>
|
|
</div>`;
|
|
};
|
|
|
|
return html`<div part="checkbox-group" class="pf-c-form__group-control pf-m-stack">
|
|
${map(kvToPairs(this.options), renderOne)}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ak-checkbox-group": CheckboxGroup;
|
|
}
|
|
}
|