lion/packages/ui/components/checkbox-group/LionCheckboxIndeterminate.js
2022-10-31 16:55:07 +01:00

251 lines
6.6 KiB
JavaScript

import { html, css } from 'lit';
import { LionCheckbox } from './LionCheckbox.js';
/**
* @typedef {import('./LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
*/
export class LionCheckboxIndeterminate extends LionCheckbox {
static get styles() {
return [
...(super.styles || []),
css`
:host .choice-field__nested-checkboxes {
display: block;
}
::slotted([slot='checkbox']) {
padding-left: 8px;
}
`,
];
}
/** @type {any} */
static get properties() {
return {
/**
* Indeterminate state of the checkbox
*/
indeterminate: {
type: Boolean,
reflect: true,
},
mixedState: {
type: Boolean,
reflect: true,
attribute: 'mixed-state',
},
};
}
/**
* @protected
*/
get _checkboxGroupNode() {
return /** @type LionCheckboxGroup */ (this._parentFormGroup);
}
/**
* @protected
*/
get _subCheckboxes() {
let checkboxes = [];
if (
this._checkboxGroupNode &&
this._checkboxGroupNode.formElements &&
this._checkboxGroupNode.formElements.length > 0
) {
checkboxes = this._checkboxGroupNode.formElements.filter(
checkbox => checkbox !== this && this.contains(checkbox),
);
}
return /** @type LionCheckbox[] */ (checkboxes);
}
_storeIndeterminateState() {
this._indeterminateSubStates = this._subCheckboxes.map(checkbox => checkbox.checked);
}
_setOldState() {
if (this.indeterminate) {
this._oldState = 'indeterminate';
} else {
this._oldState = this.checked ? 'checked' : 'unchecked';
}
}
/**
* @protected
*/
_setOwnCheckedState() {
const subCheckboxes = this._subCheckboxes;
if (!subCheckboxes.length) {
return;
}
this.__settingOwnChecked = true;
const checkedElements = subCheckboxes.filter(checkbox => checkbox.checked);
switch (subCheckboxes.length - checkedElements.length) {
// all checked
case 0:
this.indeterminate = false;
this.checked = true;
break;
// none checked
case subCheckboxes.length:
this.indeterminate = false;
this.checked = false;
break;
default:
this.indeterminate = true;
this.checked = false;
}
this.updateComplete.then(() => {
this.__settingOwnChecked = false;
});
}
_setBasedOnMixedState() {
switch (this._oldState) {
case 'checked':
// --> unchecked
this.checked = false;
this.indeterminate = false;
break;
case 'unchecked':
// --> indeterminate
this.checked = false;
this.indeterminate = true;
break;
case 'indeterminate':
// --> checked
this.checked = true;
this.indeterminate = false;
break;
// no default
}
}
/**
* @param {Event} ev
* @private
*/
__onModelValueChanged(ev) {
if (this.disabled) {
return;
}
const _ev = /** @type {CustomEvent} */ (ev);
// If the model value change event is coming from out own _inputNode
// and we're not already setting our own (mixed) state programmatically
if (_ev.detail.formPath[0] === this && !this.__settingOwnChecked) {
const allEqual = (/** @type {any[]} */ arr) => arr.every(val => val === arr[0]);
// If our child checkboxes states are all the same, we shouldn't be able to reach indeterminate (mixed) state
if (this.mixedState && !allEqual(this._indeterminateSubStates)) {
this._setBasedOnMixedState();
}
this.__settingOwnSubs = true;
const subCheckboxes = this._subCheckboxes;
const checkedElements = subCheckboxes.filter(checkbox => checkbox.checked);
const disabledElements = subCheckboxes.filter(checkbox => checkbox.disabled);
const allChecked =
subCheckboxes.length > 0 && subCheckboxes.length === checkedElements.length;
const allDisabled =
subCheckboxes.length > 0 && subCheckboxes.length === disabledElements.length;
const hasDisabledElements = disabledElements.length > 0;
if (allDisabled) {
this.checked = allChecked;
}
if (this.indeterminate && (this.mixedState || hasDisabledElements)) {
this._subCheckboxes.forEach((checkbox, i) => {
// eslint-disable-next-line no-param-reassign
checkbox.checked = this._indeterminateSubStates[i];
});
} else {
this._subCheckboxes
.filter(checkbox => !checkbox.disabled)
.forEach(checkbox => {
// eslint-disable-next-line no-param-reassign
checkbox.checked = this._inputNode.checked;
});
}
this.updateComplete.then(() => {
this.__settingOwnSubs = false;
});
} else {
this._setOwnCheckedState();
this.updateComplete.then(() => {
if (!this.__settingOwnSubs && !this.__settingOwnChecked && this.mixedState) {
this._storeIndeterminateState();
}
});
}
if (this.mixedState) {
this._setOldState();
}
}
/**
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_afterTemplate() {
return html`
<div class="choice-field__nested-checkboxes">
<slot name="checkbox"></slot>
</div>
`;
}
/**
* @protected
*/
_onRequestToAddFormElement() {
this._setOwnCheckedState();
}
constructor() {
super();
this.indeterminate = false;
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
/** @type {boolean[]} */
this._indeterminateSubStates = [];
this.mixedState = false;
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('model-value-changed', this.__onModelValueChanged);
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('model-value-changed', this.__onModelValueChanged);
this.removeEventListener('form-element-register', this._onRequestToAddFormElement);
}
/** @param {import('lit-element').PropertyValues } changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this._setOldState();
if (this.indeterminate) {
this._storeIndeterminateState();
}
}
/** @param {import('lit-element').PropertyValues } changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('indeterminate')) {
this._inputNode.indeterminate = this.indeterminate;
}
}
}