UNPKG

@aurelia-mdc-web/select

Version:

Wrapper for Material Components Web Select

399 lines 14.8 kB
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