@aurelia-mdc-web/all 9.3.0-au2 → 9.3.1-au2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chips/mdc-chip/mdc-chip.js.map +1 -1
- package/dist/data-table/mdc-data-table.js.map +1 -1
- package/dist/dialog/mdc-dialog-service.js.map +1 -1
- package/dist/expandable/mdc-expandable.js.map +1 -1
- package/dist/form-field/mdc-form-field.js.map +1 -1
- package/dist/list/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.js.map +1 -1
- package/dist/list/mdc-list-item/mdc-list-item.js.map +1 -1
- package/dist/menu/mdc-menu.js.map +1 -1
- package/dist/segmented-button/mdc-segmented-button-segment/mdc-segmented-button-segment.js.map +1 -1
- package/dist/select/mdc-select-value-observer.js.map +1 -1
- package/dist/select/mdc-select.js.map +1 -1
- package/dist/text-field/mdc-text-field.js.map +1 -1
- package/dist/tree-view/mdc-tree-view.js.map +1 -1
- package/package.json +2 -2
- package/src/chips/mdc-chip/mdc-chip.ts +290 -290
- package/src/data-table/mdc-data-table.ts +432 -432
- package/src/dialog/mdc-dialog-service.ts +80 -80
- package/src/expandable/mdc-expandable.ts +104 -104
- package/src/form-field/mdc-form-field.ts +60 -60
- package/src/list/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.ts +108 -108
- package/src/list/mdc-list-item/mdc-list-item.ts +136 -136
- package/src/menu/mdc-menu.ts +340 -340
- package/src/segmented-button/mdc-segmented-button-segment/mdc-segmented-button-segment.ts +170 -170
- package/src/select/mdc-select-value-observer.ts +346 -346
- package/src/select/mdc-select.ts +480 -480
- package/src/text-field/mdc-text-field.ts +535 -535
- package/src/tree-view/mdc-tree-view.ts +147 -147
package/src/select/mdc-select.ts
CHANGED
|
@@ -1,480 +1,480 @@
|
|
|
1
|
-
import { MdcComponent, IValidatedElement, IError, booleanAttr } from '../base';
|
|
2
|
-
import { cssClasses, MDCSelectFoundationMap, MDCSelectEventDetail, strings } from '@material/select';
|
|
3
|
-
import { inject, customElement, INode, IPlatform, bindable } from 'aurelia';
|
|
4
|
-
import { MdcSelectIcon, IMdcSelectIconElement, mdcIconStrings } from './mdc-select-icon';
|
|
5
|
-
import { MdcSelectHelperText, mdcHelperTextCssClasses } from './mdc-select-helper-text/mdc-select-helper-text';
|
|
6
|
-
import { MDCNotchedOutline } from '@material/notched-outline';
|
|
7
|
-
import { MDCMenuItemEvent, Corner } from '@material/menu';
|
|
8
|
-
import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia';
|
|
9
|
-
import { MDCSelectAdapterAurelia } from './mdc-select-adapter-aurelia';
|
|
10
|
-
import { MDCMenuDistance } from '@material/menu-surface';
|
|
11
|
-
import { processContent, BindingMode, CustomElement, CustomAttribute } from '@aurelia/runtime-html';
|
|
12
|
-
import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration';
|
|
13
|
-
import template from './mdc-select.html?raw';
|
|
14
|
-
import { MdcFloatingLabel } from '../floating-label/mdc-floating-label';
|
|
15
|
-
import { MdcLineRipple } from '../line-ripple/mdc-line-ripple';
|
|
16
|
-
import { MdcListItem } from '../list/mdc-list-item/mdc-list-item';
|
|
17
|
-
import { MdcMenu } from '../menu/mdc-menu';
|
|
18
|
-
|
|
19
|
-
strings.CHANGE_EVENT = strings.CHANGE_EVENT.toLowerCase();
|
|
20
|
-
|
|
21
|
-
let selectId = 0;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @selector mdc-select
|
|
25
|
-
* @emits mdcselect:change | Emitted if user changed the value
|
|
26
|
-
*/
|
|
27
|
-
@inject(Element, IPlatform, MdcDefaultSelectConfiguration)
|
|
28
|
-
@customElement({ name: 'mdc-select', template })
|
|
29
|
-
@processContent(function processContent(node: INode, platform: IPlatform) {
|
|
30
|
-
const el = node as Element;
|
|
31
|
-
|
|
32
|
-
const leadingIcon = el.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
|
|
33
|
-
leadingIcon?.remove();
|
|
34
|
-
|
|
35
|
-
const template = platform.document.createElement('template');
|
|
36
|
-
template.setAttribute('au-slot', '');
|
|
37
|
-
template.innerHTML = el.innerHTML;
|
|
38
|
-
el.innerHTML = '';
|
|
39
|
-
el.appendChild(template);
|
|
40
|
-
|
|
41
|
-
// move icon to the slot - this allows omitting slot specification
|
|
42
|
-
if (leadingIcon) {
|
|
43
|
-
const div = platform.document.createElement('div');
|
|
44
|
-
div.appendChild(leadingIcon);
|
|
45
|
-
const iconTemplate = platform.document.createElement('template');
|
|
46
|
-
iconTemplate.setAttribute('au-slot', 'leading-icon');
|
|
47
|
-
iconTemplate.innerHTML = div.innerHTML;
|
|
48
|
-
el.appendChild(iconTemplate);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
)
|
|
52
|
-
export class MdcSelect extends MdcComponent<MDCSelectFoundationAurelia> {
|
|
53
|
-
constructor(root: HTMLElement, private platform: IPlatform, private defaultConfiguration: MdcDefaultSelectConfiguration) {
|
|
54
|
-
super(root);
|
|
55
|
-
this.outlined = this.defaultConfiguration.outlined;
|
|
56
|
-
defineMdcSelectElementApis(this.root);
|
|
57
|
-
this.root.id = this.id;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
id: string = `mdc-select-${++selectId}`;
|
|
61
|
-
public menu: MdcMenu;
|
|
62
|
-
private selectAnchor: HTMLElement;
|
|
63
|
-
private selectedText: HTMLElement;
|
|
64
|
-
|
|
65
|
-
private menuElement?: Element;
|
|
66
|
-
|
|
67
|
-
get items(): MdcListItem[] | undefined {
|
|
68
|
-
return this.menu.list_?.items;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private leadingIcon?: MdcSelectIcon;
|
|
72
|
-
|
|
73
|
-
private helperText?: MdcSelectHelperText;
|
|
74
|
-
private lineRipple?: MdcLineRipple;
|
|
75
|
-
private mdcLabel: MdcFloatingLabel;
|
|
76
|
-
private outline?: MDCNotchedOutline;
|
|
77
|
-
errors = new Map<IError, boolean>();
|
|
78
|
-
|
|
79
|
-
/** Sets the select label */
|
|
80
|
-
@bindable()
|
|
81
|
-
label: string;
|
|
82
|
-
labelChanged() {
|
|
83
|
-
this.platform.domQueue.queueTask(() => this.foundation?.layout());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Styles the select as an outlined select */
|
|
87
|
-
@bindable({ set: booleanAttr })
|
|
88
|
-
outlined?: boolean;
|
|
89
|
-
outlinedChanged() {
|
|
90
|
-
this.platform.domQueue.queueTask(() => this.foundation?.layout());
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Makes the value required */
|
|
94
|
-
@bindable({ set: booleanAttr })
|
|
95
|
-
required: boolean;
|
|
96
|
-
requiredChanged() {
|
|
97
|
-
if (this.required) {
|
|
98
|
-
this.selectAnchor?.setAttribute('aria-required', 'true');
|
|
99
|
-
} else {
|
|
100
|
-
this.selectAnchor?.removeAttribute('aria-required');
|
|
101
|
-
}
|
|
102
|
-
this.foundation?.setRequired(this.required ?? false);
|
|
103
|
-
this.platform.domWriteQueue.queueTask(() => this.foundation?.layout());
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Enables/disables the select */
|
|
107
|
-
@bindable({ set: booleanAttr })
|
|
108
|
-
disabled?: boolean;
|
|
109
|
-
disabledChanged() {
|
|
110
|
-
if (this.disabled !== undefined) {
|
|
111
|
-
this.foundation?.setDisabled(this.disabled);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Hoists the select DOM to document.body */
|
|
116
|
-
@bindable({ set: booleanAttr, mode: BindingMode.oneTime })
|
|
117
|
-
hoistToBody: boolean;
|
|
118
|
-
|
|
119
|
-
/** Sets the select DOM position to fixed */
|
|
120
|
-
@bindable({ set: booleanAttr, mode: BindingMode.oneTime })
|
|
121
|
-
fixed: boolean;
|
|
122
|
-
|
|
123
|
-
/** Sets the margin between the select input and the dropdown */
|
|
124
|
-
@bindable()
|
|
125
|
-
anchorMargin: Partial<MDCMenuDistance>;
|
|
126
|
-
|
|
127
|
-
/** Sets the select dropdown width to match content */
|
|
128
|
-
@bindable({ set: booleanAttr })
|
|
129
|
-
naturalWidth: boolean;
|
|
130
|
-
|
|
131
|
-
private _value: unknown;
|
|
132
|
-
get value(): unknown {
|
|
133
|
-
if (this.foundation) {
|
|
134
|
-
return this.foundation.getValue();
|
|
135
|
-
} else {
|
|
136
|
-
return this._value;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
set value(value: unknown) {
|
|
141
|
-
this.setValue(value);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
setValue(value: unknown, skipNotify: boolean = false) {
|
|
145
|
-
this._value = value;
|
|
146
|
-
if (this.foundation) {
|
|
147
|
-
this.foundation.setValue(value, skipNotify);
|
|
148
|
-
this.foundation.layout();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
get valid(): boolean {
|
|
153
|
-
return this.foundation?.isValid() ?? true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
set valid(value: boolean) {
|
|
157
|
-
this.foundation?.setValid(value);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get selectedIndex(): number {
|
|
161
|
-
return this.foundation!.getSelectedIndex();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
set selectedIndex(selectedIndex: number) {
|
|
165
|
-
this.foundation?.setSelectedIndex(selectedIndex, /** closeMenu */ true);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
addError(error: IError) {
|
|
169
|
-
this.errors.set(error, true);
|
|
170
|
-
this.valid = false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
removeError(error: IError) {
|
|
174
|
-
this.errors.delete(error);
|
|
175
|
-
this.valid = this.errors.size === 0;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
renderErrors() {
|
|
179
|
-
if (this.helperText) {
|
|
180
|
-
this.helperText.errors = Array.from(this.errors.keys()).filter(x => x.message !== null).map(x => x.message!);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async attaching() {
|
|
185
|
-
const nextSibling = this.root.nextElementSibling;
|
|
186
|
-
if (nextSibling?.tagName === mdcHelperTextCssClasses.ROOT.toUpperCase()) {
|
|
187
|
-
this.helperText = CustomElement.for<MdcSelectHelperText>(nextSibling).viewModel;
|
|
188
|
-
await this.helperText.initialised;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
beforeFoundationCreated() {
|
|
193
|
-
const leadingIconEl = this.root.querySelector<IMdcSelectIconElement>(`${strings.LEADING_ICON_SELECTOR}`);
|
|
194
|
-
if (leadingIconEl) {
|
|
195
|
-
this.leadingIcon = CustomAttribute.for<MdcSelectIcon>(leadingIconEl, mdcIconStrings.ATTRIBUTE)?.viewModel;
|
|
196
|
-
}
|
|
197
|
-
this.menu.list_!.singleSelection = true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
initialSyncWithDOM() {
|
|
201
|
-
// set initial value without emitting change events
|
|
202
|
-
this.foundation?.setValue(this._value, true);
|
|
203
|
-
this.foundation?.layout();
|
|
204
|
-
this.errors = new Map<IError, boolean>();
|
|
205
|
-
this.valid = true;
|
|
206
|
-
|
|
207
|
-
this.labelChanged();
|
|
208
|
-
this.disabledChanged();
|
|
209
|
-
this.outlinedChanged();
|
|
210
|
-
this.requiredChanged();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
getDefaultFoundation() {
|
|
214
|
-
// DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
|
|
215
|
-
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
|
|
216
|
-
const adapter: MDCSelectAdapterAurelia = {
|
|
217
|
-
...this.getSelectAdapterMethods(),
|
|
218
|
-
...this.getCommonAdapterMethods(),
|
|
219
|
-
...this.getOutlineAdapterMethods(),
|
|
220
|
-
...this.getLabelAdapterMethods(),
|
|
221
|
-
};
|
|
222
|
-
return new MDCSelectFoundationAurelia(adapter, this.getFoundationMap());
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private getSelectAdapterMethods() {
|
|
226
|
-
return {
|
|
227
|
-
setSelectedText: (text: string) => {
|
|
228
|
-
this.selectedText.textContent = text;
|
|
229
|
-
},
|
|
230
|
-
isSelectAnchorFocused: () => document.activeElement === this.selectAnchor,
|
|
231
|
-
getSelectAnchorAttr: (attr: string) => this.selectAnchor.getAttribute(attr),
|
|
232
|
-
setSelectAnchorAttr: (attr: string, value: string) => {
|
|
233
|
-
this.selectAnchor.setAttribute(attr, value);
|
|
234
|
-
},
|
|
235
|
-
removeSelectAnchorAttr: (attr: string) => {
|
|
236
|
-
this.selectAnchor.removeAttribute(attr);
|
|
237
|
-
},
|
|
238
|
-
addMenuClass: (className: string) => {
|
|
239
|
-
this.menuElement?.classList.add(className);
|
|
240
|
-
},
|
|
241
|
-
removeMenuClass: (className: string) => {
|
|
242
|
-
this.menuElement?.classList.remove(className);
|
|
243
|
-
},
|
|
244
|
-
openMenu: () => {
|
|
245
|
-
this.menu.open = true;
|
|
246
|
-
this.menu.root.style.minWidth = this.menu.root.style.maxWidth = (this.hoistToBody || this.fixed) && !this.naturalWidth
|
|
247
|
-
? `${this.root.clientWidth}px`
|
|
248
|
-
: '';
|
|
249
|
-
},
|
|
250
|
-
closeMenu: () => { this.menu.open = false; },
|
|
251
|
-
getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR)!,
|
|
252
|
-
setMenuAnchorElement: (anchorEl: HTMLElement) => {
|
|
253
|
-
this.menu.anchor = anchorEl;
|
|
254
|
-
},
|
|
255
|
-
setMenuAnchorCorner: (anchorCorner: Corner) => {
|
|
256
|
-
this.menu.setAnchorCorner(anchorCorner);
|
|
257
|
-
},
|
|
258
|
-
setMenuWrapFocus: (wrapFocus: boolean) => {
|
|
259
|
-
this.menu.wrapFocus = wrapFocus;
|
|
260
|
-
},
|
|
261
|
-
getSelectedIndex: () => {
|
|
262
|
-
const index = this.menu.selectedIndex;
|
|
263
|
-
return index instanceof Array ? index[0] : index;
|
|
264
|
-
},
|
|
265
|
-
setSelectedIndex: (index: number) => {
|
|
266
|
-
this.menu.selectedIndex = index;
|
|
267
|
-
},
|
|
268
|
-
removeAttributeAtIndex: (index: number, attributeName: string) => {
|
|
269
|
-
this.menu.items[index].removeAttribute(attributeName);
|
|
270
|
-
},
|
|
271
|
-
focusMenuItemAtIndex: (index: number) => {
|
|
272
|
-
(this.menu.items[index] as HTMLElement).focus();
|
|
273
|
-
},
|
|
274
|
-
getMenuItemCount: () => this.menu.items.length,
|
|
275
|
-
getMenuItemValues: () => this.menu.items.map(x => CustomElement.for<MdcListItem>(x).viewModel.value),
|
|
276
|
-
getMenuItemTextAtIndex: (index: number) => this.menu.getPrimaryTextAtIndex(index),
|
|
277
|
-
isTypeaheadInProgress: () => this.menu.typeaheadInProgress,
|
|
278
|
-
typeaheadMatchItem: (nextChar: string, startingIndex: number) => this.menu.typeaheadMatchItem(nextChar, startingIndex),
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private getCommonAdapterMethods() {
|
|
283
|
-
return {
|
|
284
|
-
addClass: (className: string) => {
|
|
285
|
-
this.root.classList.add(className);
|
|
286
|
-
},
|
|
287
|
-
removeClass: (className: string) => {
|
|
288
|
-
this.root.classList.remove(className);
|
|
289
|
-
},
|
|
290
|
-
hasClass: (className: string) => this.root.classList.contains(className),
|
|
291
|
-
setRippleCenter: (normalizedX: number) => this.lineRipple?.setRippleCenter(normalizedX),
|
|
292
|
-
activateBottomLine: () => this.lineRipple?.activate(),
|
|
293
|
-
deactivateBottomLine: () => this.lineRipple?.deactivate(),
|
|
294
|
-
notifyChange: (value: string) => {
|
|
295
|
-
const index = this.selectedIndex;
|
|
296
|
-
this.emit<MDCSelectEventDetail>(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */);
|
|
297
|
-
this.emit<MDCSelectEventDetail>('change', { value, index }, true /* shouldBubble */);
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private getOutlineAdapterMethods() {
|
|
303
|
-
return {
|
|
304
|
-
hasOutline: () => Boolean(this.outline),
|
|
305
|
-
notchOutline: (labelWidth: number) => this.outline?.notch(labelWidth),
|
|
306
|
-
closeOutline: () => this.outline?.closeNotch(),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private getLabelAdapterMethods() {
|
|
311
|
-
return {
|
|
312
|
-
hasLabel: () => !!this.mdcLabel,
|
|
313
|
-
floatLabel: (shouldFloat: boolean) => this.mdcLabel?.float(shouldFloat),
|
|
314
|
-
getLabelWidth: () => this.mdcLabel ? this.mdcLabel.getWidth() : 0,
|
|
315
|
-
setLabelRequired: (isRequired: boolean) => this.mdcLabel?.setRequired(isRequired),
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
handleChange() {
|
|
320
|
-
this.foundation?.handleChange();
|
|
321
|
-
this.emit('change', {}, true);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
handleFocus() {
|
|
325
|
-
this.foundation?.handleFocus();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
handleBlur() {
|
|
329
|
-
this.foundation?.handleBlur();
|
|
330
|
-
// if class is set it means the menu is open,
|
|
331
|
-
// do not emit blur since "conceptually" the element is still active
|
|
332
|
-
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
|
|
333
|
-
this.emit('blur', {}, true);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
handleClick(evt: MouseEvent) {
|
|
338
|
-
this.selectAnchor.focus();
|
|
339
|
-
this.foundation?.handleClick(this.getNormalizedXCoordinate(evt));
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
handleKeydown(evt: KeyboardEvent) {
|
|
343
|
-
this.foundation?.handleKeydown(evt);
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
handleMenuItemAction(evt: MDCMenuItemEvent) {
|
|
348
|
-
this.foundation?.handleMenuItemAction(evt.detail.index);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
handleMenuOpened() {
|
|
352
|
-
this.foundation?.handleMenuOpened();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
handleMenuClosed() {
|
|
356
|
-
this.foundation?.handleMenuClosed();
|
|
357
|
-
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
|
|
358
|
-
this.emit('blur', {}, true);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
handleItemsChanged() {
|
|
363
|
-
this.foundation?.layoutOptions();
|
|
364
|
-
this.foundation?.layout();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
focus() {
|
|
368
|
-
this.selectAnchor.focus();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
blur() {
|
|
372
|
-
this.selectAnchor.blur();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* @hidden
|
|
377
|
-
* Calculates where the line ripple should start based on the x coordinate within the component.
|
|
378
|
-
*/
|
|
379
|
-
private getNormalizedXCoordinate(evt: MouseEvent | TouchEvent): number {
|
|
380
|
-
const targetClientRect = (evt.target as Element).getBoundingClientRect();
|
|
381
|
-
const xCoordinate =
|
|
382
|
-
this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
|
|
383
|
-
return xCoordinate - targetClientRect.left;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private isTouchEvent(evt: MouseEvent | TouchEvent): evt is TouchEvent {
|
|
387
|
-
return Boolean((evt as TouchEvent).touches);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* @hidden
|
|
392
|
-
* Returns a map of all subcomponents to subfoundations.
|
|
393
|
-
*/
|
|
394
|
-
private getFoundationMap(): Partial<MDCSelectFoundationMap> {
|
|
395
|
-
return {
|
|
396
|
-
helperText: this.helperText?.foundation,
|
|
397
|
-
leadingIcon: this.leadingIcon?.foundation
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/** @hidden */
|
|
403
|
-
export interface IMdcSelectElement extends IValidatedElement {
|
|
404
|
-
$au: {
|
|
405
|
-
'au:resource:custom-element': {
|
|
406
|
-
viewModel: MdcSelect;
|
|
407
|
-
};
|
|
408
|
-
};
|
|
409
|
-
value: unknown;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function defineMdcSelectElementApis(element: HTMLElement) {
|
|
413
|
-
Object.defineProperties(element, {
|
|
414
|
-
value: {
|
|
415
|
-
get(this: IMdcSelectElement) {
|
|
416
|
-
return CustomElement.for<MdcSelect>(this).viewModel.value;
|
|
417
|
-
},
|
|
418
|
-
set(this: IMdcSelectElement, value: unknown) {
|
|
419
|
-
// aurelia binding converts "undefined" and "null" into empty string
|
|
420
|
-
// this does not translate well into "empty" menu items when several selects are bound to the same field
|
|
421
|
-
CustomElement.for<MdcSelect>(this).viewModel.value = value === '' ? undefined : value;
|
|
422
|
-
},
|
|
423
|
-
configurable: true
|
|
424
|
-
},
|
|
425
|
-
options: {
|
|
426
|
-
get(this: IMdcSelectElement) {
|
|
427
|
-
return CustomElement.for<MdcSelect>(this).viewModel.root.querySelectorAll('.mdc-list-item');
|
|
428
|
-
},
|
|
429
|
-
configurable: true
|
|
430
|
-
},
|
|
431
|
-
selectedIndex: {
|
|
432
|
-
get(this: IMdcSelectElement) {
|
|
433
|
-
return CustomElement.for<MdcSelect>(this).viewModel.selectedIndex;
|
|
434
|
-
},
|
|
435
|
-
set(this: IMdcSelectElement, value: number) {
|
|
436
|
-
CustomElement.for<MdcSelect>(this).viewModel.selectedIndex = value;
|
|
437
|
-
},
|
|
438
|
-
configurable: true
|
|
439
|
-
},
|
|
440
|
-
valid: {
|
|
441
|
-
get(this: IMdcSelectElement) {
|
|
442
|
-
return CustomElement.for<MdcSelect>(this).viewModel.valid;
|
|
443
|
-
},
|
|
444
|
-
set(this: IMdcSelectElement, value: boolean) {
|
|
445
|
-
CustomElement.for<MdcSelect>(this).viewModel.valid = value;
|
|
446
|
-
},
|
|
447
|
-
configurable: true
|
|
448
|
-
},
|
|
449
|
-
addError: {
|
|
450
|
-
value(this: IMdcSelectElement, error: IError) {
|
|
451
|
-
CustomElement.for<MdcSelect>(this).viewModel.addError(error);
|
|
452
|
-
},
|
|
453
|
-
configurable: true
|
|
454
|
-
},
|
|
455
|
-
removeError: {
|
|
456
|
-
value(this: IMdcSelectElement, error: IError) {
|
|
457
|
-
CustomElement.for<MdcSelect>(this).viewModel.removeError(error);
|
|
458
|
-
},
|
|
459
|
-
configurable: true
|
|
460
|
-
},
|
|
461
|
-
renderErrors: {
|
|
462
|
-
value(this: IMdcSelectElement): void {
|
|
463
|
-
CustomElement.for<MdcSelect>(this).viewModel.renderErrors();
|
|
464
|
-
},
|
|
465
|
-
configurable: true
|
|
466
|
-
},
|
|
467
|
-
focus: {
|
|
468
|
-
value(this: IMdcSelectElement) {
|
|
469
|
-
CustomElement.for<MdcSelect>(this).viewModel.focus();
|
|
470
|
-
},
|
|
471
|
-
configurable: true
|
|
472
|
-
},
|
|
473
|
-
blur: {
|
|
474
|
-
value(this: IMdcSelectElement) {
|
|
475
|
-
CustomElement.for<MdcSelect>(this).viewModel.blur();
|
|
476
|
-
},
|
|
477
|
-
configurable: true
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
1
|
+
import { MdcComponent, IValidatedElement, IError, booleanAttr } from '../base';
|
|
2
|
+
import { cssClasses, MDCSelectFoundationMap, MDCSelectEventDetail, strings } from '@material/select';
|
|
3
|
+
import { inject, customElement, INode, IPlatform, bindable } from 'aurelia';
|
|
4
|
+
import { MdcSelectIcon, IMdcSelectIconElement, mdcIconStrings } from './mdc-select-icon';
|
|
5
|
+
import { MdcSelectHelperText, mdcHelperTextCssClasses } from './mdc-select-helper-text/mdc-select-helper-text';
|
|
6
|
+
import { MDCNotchedOutline } from '@material/notched-outline';
|
|
7
|
+
import { MDCMenuItemEvent, Corner } from '@material/menu';
|
|
8
|
+
import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia';
|
|
9
|
+
import { MDCSelectAdapterAurelia } from './mdc-select-adapter-aurelia';
|
|
10
|
+
import { MDCMenuDistance } from '@material/menu-surface';
|
|
11
|
+
import { processContent, BindingMode, CustomElement, CustomAttribute } from '@aurelia/runtime-html';
|
|
12
|
+
import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration';
|
|
13
|
+
import template from './mdc-select.html?raw';
|
|
14
|
+
import { MdcFloatingLabel } from '../floating-label/mdc-floating-label';
|
|
15
|
+
import { MdcLineRipple } from '../line-ripple/mdc-line-ripple';
|
|
16
|
+
import { MdcListItem } from '../list/mdc-list-item/mdc-list-item';
|
|
17
|
+
import { MdcMenu } from '../menu/mdc-menu';
|
|
18
|
+
|
|
19
|
+
strings.CHANGE_EVENT = strings.CHANGE_EVENT.toLowerCase();
|
|
20
|
+
|
|
21
|
+
let selectId = 0;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @selector mdc-select
|
|
25
|
+
* @emits mdcselect:change | Emitted if user changed the value
|
|
26
|
+
*/
|
|
27
|
+
@inject(Element, IPlatform, MdcDefaultSelectConfiguration)
|
|
28
|
+
@customElement({ name: 'mdc-select', template })
|
|
29
|
+
@processContent(function processContent(node: INode, platform: IPlatform) {
|
|
30
|
+
const el = node as Element;
|
|
31
|
+
|
|
32
|
+
const leadingIcon = el.querySelector(`[${mdcIconStrings.ATTRIBUTE}]`);
|
|
33
|
+
leadingIcon?.remove();
|
|
34
|
+
|
|
35
|
+
const template = platform.document.createElement('template');
|
|
36
|
+
template.setAttribute('au-slot', '');
|
|
37
|
+
template.innerHTML = el.innerHTML;
|
|
38
|
+
el.innerHTML = '';
|
|
39
|
+
el.appendChild(template);
|
|
40
|
+
|
|
41
|
+
// move icon to the slot - this allows omitting slot specification
|
|
42
|
+
if (leadingIcon) {
|
|
43
|
+
const div = platform.document.createElement('div');
|
|
44
|
+
div.appendChild(leadingIcon);
|
|
45
|
+
const iconTemplate = platform.document.createElement('template');
|
|
46
|
+
iconTemplate.setAttribute('au-slot', 'leading-icon');
|
|
47
|
+
iconTemplate.innerHTML = div.innerHTML;
|
|
48
|
+
el.appendChild(iconTemplate);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
export class MdcSelect extends MdcComponent<MDCSelectFoundationAurelia> {
|
|
53
|
+
constructor(root: HTMLElement, private platform: IPlatform, private defaultConfiguration: MdcDefaultSelectConfiguration) {
|
|
54
|
+
super(root);
|
|
55
|
+
this.outlined = this.defaultConfiguration.outlined;
|
|
56
|
+
defineMdcSelectElementApis(this.root);
|
|
57
|
+
this.root.id = this.id;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
id: string = `mdc-select-${++selectId}`;
|
|
61
|
+
public menu: MdcMenu;
|
|
62
|
+
private selectAnchor: HTMLElement;
|
|
63
|
+
private selectedText: HTMLElement;
|
|
64
|
+
|
|
65
|
+
private menuElement?: Element;
|
|
66
|
+
|
|
67
|
+
get items(): MdcListItem[] | undefined {
|
|
68
|
+
return this.menu.list_?.items;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private leadingIcon?: MdcSelectIcon;
|
|
72
|
+
|
|
73
|
+
private helperText?: MdcSelectHelperText;
|
|
74
|
+
private lineRipple?: MdcLineRipple;
|
|
75
|
+
private mdcLabel: MdcFloatingLabel;
|
|
76
|
+
private outline?: MDCNotchedOutline;
|
|
77
|
+
errors = new Map<IError, boolean>();
|
|
78
|
+
|
|
79
|
+
/** Sets the select label */
|
|
80
|
+
@bindable()
|
|
81
|
+
label: string;
|
|
82
|
+
labelChanged() {
|
|
83
|
+
this.platform.domQueue.queueTask(() => this.foundation?.layout());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Styles the select as an outlined select */
|
|
87
|
+
@bindable({ set: booleanAttr })
|
|
88
|
+
outlined?: boolean;
|
|
89
|
+
outlinedChanged() {
|
|
90
|
+
this.platform.domQueue.queueTask(() => this.foundation?.layout());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Makes the value required */
|
|
94
|
+
@bindable({ set: booleanAttr })
|
|
95
|
+
required: boolean;
|
|
96
|
+
requiredChanged() {
|
|
97
|
+
if (this.required) {
|
|
98
|
+
this.selectAnchor?.setAttribute('aria-required', 'true');
|
|
99
|
+
} else {
|
|
100
|
+
this.selectAnchor?.removeAttribute('aria-required');
|
|
101
|
+
}
|
|
102
|
+
this.foundation?.setRequired(this.required ?? false);
|
|
103
|
+
this.platform.domWriteQueue.queueTask(() => this.foundation?.layout());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Enables/disables the select */
|
|
107
|
+
@bindable({ set: booleanAttr })
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
disabledChanged() {
|
|
110
|
+
if (this.disabled !== undefined) {
|
|
111
|
+
this.foundation?.setDisabled(this.disabled);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Hoists the select DOM to document.body */
|
|
116
|
+
@bindable({ set: booleanAttr, mode: BindingMode.oneTime })
|
|
117
|
+
hoistToBody: boolean;
|
|
118
|
+
|
|
119
|
+
/** Sets the select DOM position to fixed */
|
|
120
|
+
@bindable({ set: booleanAttr, mode: BindingMode.oneTime })
|
|
121
|
+
fixed: boolean;
|
|
122
|
+
|
|
123
|
+
/** Sets the margin between the select input and the dropdown */
|
|
124
|
+
@bindable()
|
|
125
|
+
anchorMargin: Partial<MDCMenuDistance>;
|
|
126
|
+
|
|
127
|
+
/** Sets the select dropdown width to match content */
|
|
128
|
+
@bindable({ set: booleanAttr })
|
|
129
|
+
naturalWidth: boolean;
|
|
130
|
+
|
|
131
|
+
private _value: unknown;
|
|
132
|
+
get value(): unknown {
|
|
133
|
+
if (this.foundation) {
|
|
134
|
+
return this.foundation.getValue();
|
|
135
|
+
} else {
|
|
136
|
+
return this._value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
set value(value: unknown) {
|
|
141
|
+
this.setValue(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setValue(value: unknown, skipNotify: boolean = false) {
|
|
145
|
+
this._value = value;
|
|
146
|
+
if (this.foundation) {
|
|
147
|
+
this.foundation.setValue(value, skipNotify);
|
|
148
|
+
this.foundation.layout();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get valid(): boolean {
|
|
153
|
+
return this.foundation?.isValid() ?? true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
set valid(value: boolean) {
|
|
157
|
+
this.foundation?.setValid(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get selectedIndex(): number {
|
|
161
|
+
return this.foundation!.getSelectedIndex();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
set selectedIndex(selectedIndex: number) {
|
|
165
|
+
this.foundation?.setSelectedIndex(selectedIndex, /** closeMenu */ true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
addError(error: IError) {
|
|
169
|
+
this.errors.set(error, true);
|
|
170
|
+
this.valid = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
removeError(error: IError) {
|
|
174
|
+
this.errors.delete(error);
|
|
175
|
+
this.valid = this.errors.size === 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
renderErrors() {
|
|
179
|
+
if (this.helperText) {
|
|
180
|
+
this.helperText.errors = Array.from(this.errors.keys()).filter(x => x.message !== null).map(x => x.message!);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async attaching() {
|
|
185
|
+
const nextSibling = this.root.nextElementSibling;
|
|
186
|
+
if (nextSibling?.tagName === mdcHelperTextCssClasses.ROOT.toUpperCase()) {
|
|
187
|
+
this.helperText = CustomElement.for<MdcSelectHelperText>(nextSibling).viewModel;
|
|
188
|
+
await this.helperText.initialised;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
beforeFoundationCreated() {
|
|
193
|
+
const leadingIconEl = this.root.querySelector<IMdcSelectIconElement>(`${strings.LEADING_ICON_SELECTOR}`);
|
|
194
|
+
if (leadingIconEl) {
|
|
195
|
+
this.leadingIcon = CustomAttribute.for<MdcSelectIcon>(leadingIconEl, mdcIconStrings.ATTRIBUTE)?.viewModel;
|
|
196
|
+
}
|
|
197
|
+
this.menu.list_!.singleSelection = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
initialSyncWithDOM() {
|
|
201
|
+
// set initial value without emitting change events
|
|
202
|
+
this.foundation?.setValue(this._value, true);
|
|
203
|
+
this.foundation?.layout();
|
|
204
|
+
this.errors = new Map<IError, boolean>();
|
|
205
|
+
this.valid = true;
|
|
206
|
+
|
|
207
|
+
this.labelChanged();
|
|
208
|
+
this.disabledChanged();
|
|
209
|
+
this.outlinedChanged();
|
|
210
|
+
this.requiredChanged();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getDefaultFoundation() {
|
|
214
|
+
// DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
|
|
215
|
+
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
|
|
216
|
+
const adapter: MDCSelectAdapterAurelia = {
|
|
217
|
+
...this.getSelectAdapterMethods(),
|
|
218
|
+
...this.getCommonAdapterMethods(),
|
|
219
|
+
...this.getOutlineAdapterMethods(),
|
|
220
|
+
...this.getLabelAdapterMethods(),
|
|
221
|
+
};
|
|
222
|
+
return new MDCSelectFoundationAurelia(adapter, this.getFoundationMap());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private getSelectAdapterMethods() {
|
|
226
|
+
return {
|
|
227
|
+
setSelectedText: (text: string) => {
|
|
228
|
+
this.selectedText.textContent = text;
|
|
229
|
+
},
|
|
230
|
+
isSelectAnchorFocused: () => document.activeElement === this.selectAnchor,
|
|
231
|
+
getSelectAnchorAttr: (attr: string) => this.selectAnchor.getAttribute(attr),
|
|
232
|
+
setSelectAnchorAttr: (attr: string, value: string) => {
|
|
233
|
+
this.selectAnchor.setAttribute(attr, value);
|
|
234
|
+
},
|
|
235
|
+
removeSelectAnchorAttr: (attr: string) => {
|
|
236
|
+
this.selectAnchor.removeAttribute(attr);
|
|
237
|
+
},
|
|
238
|
+
addMenuClass: (className: string) => {
|
|
239
|
+
this.menuElement?.classList.add(className);
|
|
240
|
+
},
|
|
241
|
+
removeMenuClass: (className: string) => {
|
|
242
|
+
this.menuElement?.classList.remove(className);
|
|
243
|
+
},
|
|
244
|
+
openMenu: () => {
|
|
245
|
+
this.menu.open = true;
|
|
246
|
+
this.menu.root.style.minWidth = this.menu.root.style.maxWidth = (this.hoistToBody || this.fixed) && !this.naturalWidth
|
|
247
|
+
? `${this.root.clientWidth}px`
|
|
248
|
+
: '';
|
|
249
|
+
},
|
|
250
|
+
closeMenu: () => { this.menu.open = false; },
|
|
251
|
+
getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR)!,
|
|
252
|
+
setMenuAnchorElement: (anchorEl: HTMLElement) => {
|
|
253
|
+
this.menu.anchor = anchorEl;
|
|
254
|
+
},
|
|
255
|
+
setMenuAnchorCorner: (anchorCorner: Corner) => {
|
|
256
|
+
this.menu.setAnchorCorner(anchorCorner);
|
|
257
|
+
},
|
|
258
|
+
setMenuWrapFocus: (wrapFocus: boolean) => {
|
|
259
|
+
this.menu.wrapFocus = wrapFocus;
|
|
260
|
+
},
|
|
261
|
+
getSelectedIndex: () => {
|
|
262
|
+
const index = this.menu.selectedIndex;
|
|
263
|
+
return index instanceof Array ? index[0] : index;
|
|
264
|
+
},
|
|
265
|
+
setSelectedIndex: (index: number) => {
|
|
266
|
+
this.menu.selectedIndex = index;
|
|
267
|
+
},
|
|
268
|
+
removeAttributeAtIndex: (index: number, attributeName: string) => {
|
|
269
|
+
this.menu.items[index].removeAttribute(attributeName);
|
|
270
|
+
},
|
|
271
|
+
focusMenuItemAtIndex: (index: number) => {
|
|
272
|
+
(this.menu.items[index] as HTMLElement).focus();
|
|
273
|
+
},
|
|
274
|
+
getMenuItemCount: () => this.menu.items.length,
|
|
275
|
+
getMenuItemValues: () => this.menu.items.map(x => CustomElement.for<MdcListItem>(x).viewModel.value),
|
|
276
|
+
getMenuItemTextAtIndex: (index: number) => this.menu.getPrimaryTextAtIndex(index),
|
|
277
|
+
isTypeaheadInProgress: () => this.menu.typeaheadInProgress,
|
|
278
|
+
typeaheadMatchItem: (nextChar: string, startingIndex: number) => this.menu.typeaheadMatchItem(nextChar, startingIndex),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private getCommonAdapterMethods() {
|
|
283
|
+
return {
|
|
284
|
+
addClass: (className: string) => {
|
|
285
|
+
this.root.classList.add(className);
|
|
286
|
+
},
|
|
287
|
+
removeClass: (className: string) => {
|
|
288
|
+
this.root.classList.remove(className);
|
|
289
|
+
},
|
|
290
|
+
hasClass: (className: string) => this.root.classList.contains(className),
|
|
291
|
+
setRippleCenter: (normalizedX: number) => this.lineRipple?.setRippleCenter(normalizedX),
|
|
292
|
+
activateBottomLine: () => this.lineRipple?.activate(),
|
|
293
|
+
deactivateBottomLine: () => this.lineRipple?.deactivate(),
|
|
294
|
+
notifyChange: (value: string) => {
|
|
295
|
+
const index = this.selectedIndex;
|
|
296
|
+
this.emit<MDCSelectEventDetail>(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */);
|
|
297
|
+
this.emit<MDCSelectEventDetail>('change', { value, index }, true /* shouldBubble */);
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private getOutlineAdapterMethods() {
|
|
303
|
+
return {
|
|
304
|
+
hasOutline: () => Boolean(this.outline),
|
|
305
|
+
notchOutline: (labelWidth: number) => this.outline?.notch(labelWidth),
|
|
306
|
+
closeOutline: () => this.outline?.closeNotch(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private getLabelAdapterMethods() {
|
|
311
|
+
return {
|
|
312
|
+
hasLabel: () => !!this.mdcLabel,
|
|
313
|
+
floatLabel: (shouldFloat: boolean) => this.mdcLabel?.float(shouldFloat),
|
|
314
|
+
getLabelWidth: () => this.mdcLabel ? this.mdcLabel.getWidth() : 0,
|
|
315
|
+
setLabelRequired: (isRequired: boolean) => this.mdcLabel?.setRequired(isRequired),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
handleChange() {
|
|
320
|
+
this.foundation?.handleChange();
|
|
321
|
+
this.emit('change', {}, true);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
handleFocus() {
|
|
325
|
+
this.foundation?.handleFocus();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
handleBlur() {
|
|
329
|
+
this.foundation?.handleBlur();
|
|
330
|
+
// if class is set it means the menu is open,
|
|
331
|
+
// do not emit blur since "conceptually" the element is still active
|
|
332
|
+
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
|
|
333
|
+
this.emit('blur', {}, true);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
handleClick(evt: MouseEvent) {
|
|
338
|
+
this.selectAnchor.focus();
|
|
339
|
+
this.foundation?.handleClick(this.getNormalizedXCoordinate(evt));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
handleKeydown(evt: KeyboardEvent) {
|
|
343
|
+
this.foundation?.handleKeydown(evt);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
handleMenuItemAction(evt: MDCMenuItemEvent) {
|
|
348
|
+
this.foundation?.handleMenuItemAction(evt.detail.index);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
handleMenuOpened() {
|
|
352
|
+
this.foundation?.handleMenuOpened();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
handleMenuClosed() {
|
|
356
|
+
this.foundation?.handleMenuClosed();
|
|
357
|
+
if (!this.root.classList.contains(cssClasses.FOCUSED)) {
|
|
358
|
+
this.emit('blur', {}, true);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
handleItemsChanged() {
|
|
363
|
+
this.foundation?.layoutOptions();
|
|
364
|
+
this.foundation?.layout();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
focus() {
|
|
368
|
+
this.selectAnchor.focus();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
blur() {
|
|
372
|
+
this.selectAnchor.blur();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @hidden
|
|
377
|
+
* Calculates where the line ripple should start based on the x coordinate within the component.
|
|
378
|
+
*/
|
|
379
|
+
private getNormalizedXCoordinate(evt: MouseEvent | TouchEvent): number {
|
|
380
|
+
const targetClientRect = (evt.target as Element).getBoundingClientRect();
|
|
381
|
+
const xCoordinate =
|
|
382
|
+
this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
|
|
383
|
+
return xCoordinate - targetClientRect.left;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private isTouchEvent(evt: MouseEvent | TouchEvent): evt is TouchEvent {
|
|
387
|
+
return Boolean((evt as TouchEvent).touches);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @hidden
|
|
392
|
+
* Returns a map of all subcomponents to subfoundations.
|
|
393
|
+
*/
|
|
394
|
+
private getFoundationMap(): Partial<MDCSelectFoundationMap> {
|
|
395
|
+
return {
|
|
396
|
+
helperText: this.helperText?.foundation,
|
|
397
|
+
leadingIcon: this.leadingIcon?.foundation
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** @hidden */
|
|
403
|
+
export interface IMdcSelectElement extends IValidatedElement {
|
|
404
|
+
$au: {
|
|
405
|
+
'au:resource:custom-element': {
|
|
406
|
+
viewModel: MdcSelect;
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
value: unknown;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function defineMdcSelectElementApis(element: HTMLElement) {
|
|
413
|
+
Object.defineProperties(element, {
|
|
414
|
+
value: {
|
|
415
|
+
get(this: IMdcSelectElement) {
|
|
416
|
+
return CustomElement.for<MdcSelect>(this).viewModel.value;
|
|
417
|
+
},
|
|
418
|
+
set(this: IMdcSelectElement, value: unknown) {
|
|
419
|
+
// aurelia binding converts "undefined" and "null" into empty string
|
|
420
|
+
// this does not translate well into "empty" menu items when several selects are bound to the same field
|
|
421
|
+
CustomElement.for<MdcSelect>(this).viewModel.value = value === '' ? undefined : value;
|
|
422
|
+
},
|
|
423
|
+
configurable: true
|
|
424
|
+
},
|
|
425
|
+
options: {
|
|
426
|
+
get(this: IMdcSelectElement) {
|
|
427
|
+
return CustomElement.for<MdcSelect>(this).viewModel.root.querySelectorAll('.mdc-list-item');
|
|
428
|
+
},
|
|
429
|
+
configurable: true
|
|
430
|
+
},
|
|
431
|
+
selectedIndex: {
|
|
432
|
+
get(this: IMdcSelectElement) {
|
|
433
|
+
return CustomElement.for<MdcSelect>(this).viewModel.selectedIndex;
|
|
434
|
+
},
|
|
435
|
+
set(this: IMdcSelectElement, value: number) {
|
|
436
|
+
CustomElement.for<MdcSelect>(this).viewModel.selectedIndex = value;
|
|
437
|
+
},
|
|
438
|
+
configurable: true
|
|
439
|
+
},
|
|
440
|
+
valid: {
|
|
441
|
+
get(this: IMdcSelectElement) {
|
|
442
|
+
return CustomElement.for<MdcSelect>(this).viewModel.valid;
|
|
443
|
+
},
|
|
444
|
+
set(this: IMdcSelectElement, value: boolean) {
|
|
445
|
+
CustomElement.for<MdcSelect>(this).viewModel.valid = value;
|
|
446
|
+
},
|
|
447
|
+
configurable: true
|
|
448
|
+
},
|
|
449
|
+
addError: {
|
|
450
|
+
value(this: IMdcSelectElement, error: IError) {
|
|
451
|
+
CustomElement.for<MdcSelect>(this).viewModel.addError(error);
|
|
452
|
+
},
|
|
453
|
+
configurable: true
|
|
454
|
+
},
|
|
455
|
+
removeError: {
|
|
456
|
+
value(this: IMdcSelectElement, error: IError) {
|
|
457
|
+
CustomElement.for<MdcSelect>(this).viewModel.removeError(error);
|
|
458
|
+
},
|
|
459
|
+
configurable: true
|
|
460
|
+
},
|
|
461
|
+
renderErrors: {
|
|
462
|
+
value(this: IMdcSelectElement): void {
|
|
463
|
+
CustomElement.for<MdcSelect>(this).viewModel.renderErrors();
|
|
464
|
+
},
|
|
465
|
+
configurable: true
|
|
466
|
+
},
|
|
467
|
+
focus: {
|
|
468
|
+
value(this: IMdcSelectElement) {
|
|
469
|
+
CustomElement.for<MdcSelect>(this).viewModel.focus();
|
|
470
|
+
},
|
|
471
|
+
configurable: true
|
|
472
|
+
},
|
|
473
|
+
blur: {
|
|
474
|
+
value(this: IMdcSelectElement) {
|
|
475
|
+
CustomElement.for<MdcSelect>(this).viewModel.blur();
|
|
476
|
+
},
|
|
477
|
+
configurable: true
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|