@aurelia-mdc-web/select
Version:
Wrapper for Material Components Web Select
399 lines • 14.8 kB
JavaScript
import { __decorate, __metadata } from "tslib";
import { MdcComponent } from '@aurelia-mdc-web/base';
import { cssClasses, strings } from '@material/select';
import { inject, useView, customElement, processContent, children, bindingMode, TaskQueue } from 'aurelia-framework';
import { PLATFORM } from 'aurelia-pal';
import { mdcIconStrings } from './mdc-select-icon';
import { mdcHelperTextCssClasses } from './mdc-select-helper-text/mdc-select-helper-text';
import { bindable } from 'aurelia-typed-observable-plugin';
import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia';
import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration';
strings.CHANGE_EVENT = strings.CHANGE_EVENT.toLowerCase();
let selectId = 0;
/**
* @selector mdc-select
* @emits mdcselect:change | Emitted if user changed the value
*/
let MdcSelect = class MdcSelect extends MdcComponent {
static processContent(_viewCompiler, _resources, element) {
// move icon to the slot - this allows omitting slot specification
const leadingIcon = element.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
leadingIcon?.setAttribute('slot', 'leading-icon');
return true;
}
constructor(root, taskQueue, defaultConfiguration) {
super(root);
this.taskQueue = taskQueue;
this.defaultConfiguration = defaultConfiguration;
this.id = `mdc-select-${++selectId}`;
this.errors = new Map();
/** Styles the select as an outlined select */
this.outlined = this.defaultConfiguration.outlined;
defineMdcSelectElementApis(this.root);
}
labelChanged() {
this.taskQueue.queueTask(() => this.foundation?.layout());
}
outlinedChanged() {
this.taskQueue.queueTask(() => this.foundation?.layout());
}
async requiredChanged() {
await this.initialised;
if (this.required) {
this.selectAnchor?.setAttribute('aria-required', 'true');
}
else {
this.selectAnchor?.removeAttribute('aria-required');
}
this.foundation?.setRequired(this.required);
this.taskQueue.queueTask(() => this.foundation?.layout());
}
async disabledChanged() {
await this.initialised;
this.foundation?.setDisabled(this.disabled);
}
get value() {
if (this.foundation) {
return this.foundation.getValue();
}
else {
return this._value;
}
}
set value(value) {
this.setValue(value);
}
setValue(value, skipNotify = false) {
this._value = value;
if (this.foundation) {
this.foundation.setValue(value, skipNotify);
this.foundation.layout();
}
}
get valid() {
return this.foundation?.isValid() ?? true;
}
set valid(value) {
this.foundation?.setValid(value);
}
get selectedIndex() {
return this.foundation.getSelectedIndex();
}
set selectedIndex(selectedIndex) {
this.foundation?.setSelectedIndex(selectedIndex, /** closeMenu */ true);
}
addError(error) {
this.errors.set(error, true);
this.valid = false;
}
removeError(error) {
this.errors.delete(error);
this.valid = this.errors.size === 0;
}
renderErrors() {
const helperText = this.root.nextElementSibling;
if (helperText?.tagName === 'MDC-SELECT-HELPER-TEXT') {
helperText.au.controller.viewModel.errors = Array.from(this.errors.keys())
.filter(x => x.message !== null).map(x => x.message);
}
}
async initialise() {
const leadingIconEl = this.root.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
this.leadingIcon = leadingIconEl?.au['mdc-select-icon'].viewModel;
const nextSibling = this.root.nextElementSibling;
if (nextSibling?.tagName === mdcHelperTextCssClasses.ROOT.toUpperCase()) {
this.helperText = nextSibling.au.controller.viewModel;
}
await Promise.all([this.helperText?.initialised, this.menu.initialised].filter(x => x));
this.menu.list_.singleSelection = true;
}
initialSyncWithDOM() {
// set initial value without emitting change events
this.foundation?.setValue(this._value, true);
this.foundation?.layout();
this.errors = new Map();
this.valid = true;
}
getDefaultFoundation() {
// DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
const adapter = {
...this.getSelectAdapterMethods(),
...this.getCommonAdapterMethods(),
...this.getOutlineAdapterMethods(),
...this.getLabelAdapterMethods(),
};
return new MDCSelectFoundationAurelia(adapter, this.getFoundationMap());
}
getSelectAdapterMethods() {
return {
setSelectedText: (text) => {
this.selectedText.textContent = text;
},
isSelectAnchorFocused: () => document.activeElement === this.selectAnchor,
getSelectAnchorAttr: (attr) => this.selectAnchor.getAttribute(attr),
setSelectAnchorAttr: (attr, value) => {
this.selectAnchor.setAttribute(attr, value);
},
removeSelectAnchorAttr: (attr) => {
this.selectAnchor.removeAttribute(attr);
},
addMenuClass: (className) => {
this.menuElement?.classList.add(className);
},
removeMenuClass: (className) => {
this.menuElement?.classList.remove(className);
},
openMenu: () => {
this.menu.open = true;
this.menu.root.style.minWidth = this.menu.root.style.maxWidth = (this.hoistToBody || this.fixed) && !this.naturalWidth
? `${this.root.clientWidth}px`
: '';
},
closeMenu: () => { this.menu.open = false; },
getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR),
setMenuAnchorElement: (anchorEl) => {
this.menu.anchor = anchorEl;
},
setMenuAnchorCorner: (anchorCorner) => {
this.menu.setAnchorCorner(anchorCorner);
},
setMenuWrapFocus: (wrapFocus) => {
this.menu.wrapFocus = wrapFocus;
},
getSelectedIndex: () => {
const index = this.menu.selectedIndex;
return index instanceof Array ? index[0] : index;
},
setSelectedIndex: (index) => {
this.menu.selectedIndex = index;
},
removeAttributeAtIndex: (index, attributeName) => {
this.menu.items[index].removeAttribute(attributeName);
},
focusMenuItemAtIndex: (index) => {
this.menu.items[index].focus();
},
getMenuItemCount: () => this.menu.items.length,
getMenuItemValues: () => this.menu.items.map(x => x.au.controller.viewModel.value),
getMenuItemTextAtIndex: (index) => this.menu.getPrimaryTextAtIndex(index),
isTypeaheadInProgress: () => this.menu.typeaheadInProgress,
typeaheadMatchItem: (nextChar, startingIndex) => this.menu.typeaheadMatchItem(nextChar, startingIndex),
};
}
getCommonAdapterMethods() {
return {
addClass: (className) => {
this.root.classList.add(className);
},
removeClass: (className) => {
this.root.classList.remove(className);
},
hasClass: (className) => this.root.classList.contains(className),
setRippleCenter: (normalizedX) => this.lineRipple?.setRippleCenter(normalizedX),
activateBottomLine: () => this.lineRipple?.activate(),
deactivateBottomLine: () => this.lineRipple?.deactivate(),
notifyChange: (value) => {
const index = this.selectedIndex;
this.emit(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */);
this.emit('change', { value, index }, true /* shouldBubble */);
},
};
}
getOutlineAdapterMethods() {
return {
hasOutline: () => Boolean(this.outline),
notchOutline: (labelWidth) => this.outline?.notch(labelWidth),
closeOutline: () => this.outline?.closeNotch(),
};
}
getLabelAdapterMethods() {
return {
hasLabel: () => !!this.mdcLabel,
floatLabel: (shouldFloat) => this.mdcLabel?.float(shouldFloat),
getLabelWidth: () => this.mdcLabel ? this.mdcLabel.getWidth() : 0,
setLabelRequired: (isRequired) => this.mdcLabel?.setRequired(isRequired),
};
}
handleChange() {
this.foundation?.handleChange();
this.emit('change', {}, true);
}
handleFocus() {
this.foundation?.handleFocus();
}
handleBlur() {
this.foundation?.handleBlur();
// if class is set it means the menu is open,
// do not emit blur since "conceptually" the element is still active
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
this.emit('blur', {}, true);
}
}
handleClick(evt) {
this.selectAnchor.focus();
this.foundation?.handleClick(this.getNormalizedXCoordinate(evt));
}
handleKeydown(evt) {
this.foundation?.handleKeydown(evt);
return true;
}
handleMenuItemAction(evt) {
this.foundation?.handleMenuItemAction(evt.detail.index);
}
handleMenuOpened() {
this.foundation?.handleMenuOpened();
}
handleMenuClosed() {
this.foundation?.handleMenuClosed();
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
this.emit('blur', {}, true);
}
}
handleItemsChanged() {
this.foundation?.layoutOptions();
this.foundation?.layout();
}
focus() {
this.selectAnchor.focus();
}
blur() {
this.selectAnchor.blur();
}
/**
* @hidden
* Calculates where the line ripple should start based on the x coordinate within the component.
*/
getNormalizedXCoordinate(evt) {
const targetClientRect = evt.target.getBoundingClientRect();
const xCoordinate = this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
return xCoordinate - targetClientRect.left;
}
isTouchEvent(evt) {
return Boolean(evt.touches);
}
/**
* @hidden
* Returns a map of all subcomponents to subfoundations.
*/
getFoundationMap() {
return {
helperText: this.helperText?.foundation,
leadingIcon: this.leadingIcon?.foundation
};
}
};
__decorate([
children('mdc-list-items'),
__metadata("design:type", Array)
], MdcSelect.prototype, "items", void 0);
__decorate([
bindable.none,
__metadata("design:type", String)
], MdcSelect.prototype, "label", void 0);
__decorate([
bindable.booleanAttr,
__metadata("design:type", Boolean)
], MdcSelect.prototype, "outlined", void 0);
__decorate([
bindable.booleanAttr,
__metadata("design:type", Boolean)
], MdcSelect.prototype, "required", void 0);
__decorate([
bindable.booleanAttr,
__metadata("design:type", Boolean)
], MdcSelect.prototype, "disabled", void 0);
__decorate([
bindable.booleanAttr({ defaultBindingMode: bindingMode.oneTime }),
__metadata("design:type", Boolean)
], MdcSelect.prototype, "hoistToBody", void 0);
__decorate([
bindable.booleanAttr({ defaultBindingMode: bindingMode.oneTime }),
__metadata("design:type", Boolean)
], MdcSelect.prototype, "fixed", void 0);
__decorate([
bindable.none,
__metadata("design:type", Object)
], MdcSelect.prototype, "anchorMargin", void 0);
__decorate([
bindable.booleanAttr,
__metadata("design:type", Boolean)
], MdcSelect.prototype, "naturalWidth", void 0);
MdcSelect = __decorate([
inject(Element, TaskQueue, MdcDefaultSelectConfiguration),
useView(PLATFORM.moduleName('./mdc-select.html')),
customElement(cssClasses.ROOT),
processContent(MdcSelect.processContent),
__metadata("design:paramtypes", [HTMLElement, TaskQueue, MdcDefaultSelectConfiguration])
], MdcSelect);
export { MdcSelect };
function defineMdcSelectElementApis(element) {
Object.defineProperties(element, {
value: {
get() {
return this.au.controller.viewModel.value;
},
set(value) {
// aurelia binding converts "undefined" and "null" into empty string
// this does not translate well into "empty" menu items when several selects are bound to the same field
this.au.controller.viewModel.value = value === '' ? undefined : value;
},
configurable: true
},
options: {
get() {
return this.au.controller.viewModel.root.querySelectorAll('.mdc-list-item');
},
configurable: true
},
selectedIndex: {
get() {
return this.au.controller.viewModel.selectedIndex;
},
set(value) {
this.au.controller.viewModel.selectedIndex = value;
},
configurable: true
},
valid: {
get() {
return this.au.controller.viewModel.valid;
},
set(value) {
this.au.controller.viewModel.valid = value;
},
configurable: true
},
addError: {
value(error) {
this.au.controller.viewModel.addError(error);
},
configurable: true
},
removeError: {
value(error) {
this.au.controller.viewModel.removeError(error);
},
configurable: true
},
renderErrors: {
value() {
this.au.controller.viewModel.renderErrors();
},
configurable: true
},
focus: {
value() {
this.au.controller.viewModel.focus();
},
configurable: true
},
blur: {
value() {
this.au.controller.viewModel.blur();
},
configurable: true
}
});
}
//# sourceMappingURL=mdc-select.js.map