UNPKG

21.2 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2016 Google Inc.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 * THE SOFTWARE.
22 */
23import { __assign, __extends } from "tslib";
24import { MDCComponent } from '@material/base/component';
25import { MDCFloatingLabel } from '@material/floating-label/component';
26import { MDCLineRipple } from '@material/line-ripple/component';
27import * as menuSurfaceConstants from '@material/menu-surface/constants';
28import { MDCMenu } from '@material/menu/component';
29import * as menuConstants from '@material/menu/constants';
30import { MDCNotchedOutline } from '@material/notched-outline/component';
31import { MDCRipple } from '@material/ripple/component';
32import { MDCRippleFoundation } from '@material/ripple/foundation';
33import { cssClasses, strings } from './constants';
34import { MDCSelectFoundation } from './foundation';
35import { MDCSelectHelperText } from './helper-text/component';
36import { MDCSelectIcon } from './icon/component';
37var MDCSelect = /** @class */ (function (_super) {
38 __extends(MDCSelect, _super);
39 function MDCSelect() {
40 return _super !== null && _super.apply(this, arguments) || this;
41 }
42 MDCSelect.attachTo = function (root) {
43 return new MDCSelect(root);
44 };
45 MDCSelect.prototype.initialize = function (labelFactory, lineRippleFactory, outlineFactory, menuFactory, iconFactory, helperTextFactory) {
46 if (labelFactory === void 0) { labelFactory = function (el) { return new MDCFloatingLabel(el); }; }
47 if (lineRippleFactory === void 0) { lineRippleFactory = function (el) { return new MDCLineRipple(el); }; }
48 if (outlineFactory === void 0) { outlineFactory = function (el) { return new MDCNotchedOutline(el); }; }
49 if (menuFactory === void 0) { menuFactory = function (el) { return new MDCMenu(el); }; }
50 if (iconFactory === void 0) { iconFactory = function (el) { return new MDCSelectIcon(el); }; }
51 if (helperTextFactory === void 0) { helperTextFactory = function (el) { return new MDCSelectHelperText(el); }; }
52 this.selectAnchor =
53 this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR);
54 this.selectedText =
55 this.root.querySelector(strings.SELECTED_TEXT_SELECTOR);
56 this.hiddenInput = this.root.querySelector(strings.HIDDEN_INPUT_SELECTOR);
57 if (!this.selectedText) {
58 throw new Error('MDCSelect: Missing required element: The following selector must be present: ' +
59 ("'" + strings.SELECTED_TEXT_SELECTOR + "'"));
60 }
61 if (this.selectAnchor.hasAttribute(strings.ARIA_CONTROLS)) {
62 var helperTextElement = document.getElementById(this.selectAnchor.getAttribute(strings.ARIA_CONTROLS));
63 if (helperTextElement) {
64 this.helperText = helperTextFactory(helperTextElement);
65 }
66 }
67 this.menuSetup(menuFactory);
68 var labelElement = this.root.querySelector(strings.LABEL_SELECTOR);
69 this.label = labelElement ? labelFactory(labelElement) : null;
70 var lineRippleElement = this.root.querySelector(strings.LINE_RIPPLE_SELECTOR);
71 this.lineRipple =
72 lineRippleElement ? lineRippleFactory(lineRippleElement) : null;
73 var outlineElement = this.root.querySelector(strings.OUTLINE_SELECTOR);
74 this.outline = outlineElement ? outlineFactory(outlineElement) : null;
75 var leadingIcon = this.root.querySelector(strings.LEADING_ICON_SELECTOR);
76 if (leadingIcon) {
77 this.leadingIcon = iconFactory(leadingIcon);
78 }
79 if (!this.root.classList.contains(cssClasses.OUTLINED)) {
80 this.ripple = this.createRipple();
81 }
82 };
83 /**
84 * Initializes the select's event listeners and internal state based
85 * on the environment's state.
86 */
87 MDCSelect.prototype.initialSyncWithDOM = function () {
88 var _this = this;
89 this.handleFocus = function () {
90 _this.foundation.handleFocus();
91 };
92 this.handleBlur = function () {
93 _this.foundation.handleBlur();
94 };
95 this.handleClick = function (evt) {
96 _this.selectAnchor.focus();
97 _this.foundation.handleClick(_this.getNormalizedXCoordinate(evt));
98 };
99 this.handleKeydown = function (evt) {
100 _this.foundation.handleKeydown(evt);
101 };
102 this.handleMenuItemAction = function (evt) {
103 _this.foundation.handleMenuItemAction(evt.detail.index);
104 };
105 this.handleMenuOpened = function () {
106 _this.foundation.handleMenuOpened();
107 };
108 this.handleMenuClosed = function () {
109 _this.foundation.handleMenuClosed();
110 };
111 this.handleMenuClosing = function () {
112 _this.foundation.handleMenuClosing();
113 };
114 this.selectAnchor.addEventListener('focus', this.handleFocus);
115 this.selectAnchor.addEventListener('blur', this.handleBlur);
116 this.selectAnchor.addEventListener('click', this.handleClick);
117 this.selectAnchor.addEventListener('keydown', this.handleKeydown);
118 this.menu.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed);
119 this.menu.listen(menuSurfaceConstants.strings.CLOSING_EVENT, this.handleMenuClosing);
120 this.menu.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened);
121 this.menu.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuItemAction);
122 if (this.hiddenInput) {
123 if (this.hiddenInput.value) {
124 // If the hidden input already has a value, use it to restore the
125 // select's value. This can happen e.g. if the user goes back or (in
126 // some browsers) refreshes the page.
127 this.foundation.setValue(this.hiddenInput.value, /** skipNotify */ true);
128 this.foundation.layout();
129 return;
130 }
131 this.hiddenInput.value = this.value;
132 }
133 };
134 MDCSelect.prototype.destroy = function () {
135 this.selectAnchor.removeEventListener('focus', this.handleFocus);
136 this.selectAnchor.removeEventListener('blur', this.handleBlur);
137 this.selectAnchor.removeEventListener('keydown', this.handleKeydown);
138 this.selectAnchor.removeEventListener('click', this.handleClick);
139 this.menu.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed);
140 this.menu.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened);
141 this.menu.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuItemAction);
142 this.menu.destroy();
143 if (this.ripple) {
144 this.ripple.destroy();
145 }
146 if (this.outline) {
147 this.outline.destroy();
148 }
149 if (this.leadingIcon) {
150 this.leadingIcon.destroy();
151 }
152 if (this.helperText) {
153 this.helperText.destroy();
154 }
155 _super.prototype.destroy.call(this);
156 };
157 Object.defineProperty(MDCSelect.prototype, "value", {
158 get: function () {
159 return this.foundation.getValue();
160 },
161 set: function (value) {
162 this.foundation.setValue(value);
163 },
164 enumerable: false,
165 configurable: true
166 });
167 MDCSelect.prototype.setValue = function (value, skipNotify) {
168 if (skipNotify === void 0) { skipNotify = false; }
169 this.foundation.setValue(value, skipNotify);
170 };
171 Object.defineProperty(MDCSelect.prototype, "selectedIndex", {
172 get: function () {
173 return this.foundation.getSelectedIndex();
174 },
175 set: function (selectedIndex) {
176 this.foundation.setSelectedIndex(selectedIndex, /* closeMenu */ true);
177 },
178 enumerable: false,
179 configurable: true
180 });
181 MDCSelect.prototype.setSelectedIndex = function (selectedIndex, skipNotify) {
182 if (skipNotify === void 0) { skipNotify = false; }
183 this.foundation.setSelectedIndex(selectedIndex, /* closeMenu */ true, skipNotify);
184 };
185 Object.defineProperty(MDCSelect.prototype, "disabled", {
186 get: function () {
187 return this.foundation.getDisabled();
188 },
189 set: function (disabled) {
190 this.foundation.setDisabled(disabled);
191 if (this.hiddenInput) {
192 this.hiddenInput.disabled = disabled;
193 }
194 },
195 enumerable: false,
196 configurable: true
197 });
198 Object.defineProperty(MDCSelect.prototype, "leadingIconAriaLabel", {
199 set: function (label) {
200 this.foundation.setLeadingIconAriaLabel(label);
201 },
202 enumerable: false,
203 configurable: true
204 });
205 Object.defineProperty(MDCSelect.prototype, "leadingIconContent", {
206 /**
207 * Sets the text content of the leading icon.
208 */
209 set: function (content) {
210 this.foundation.setLeadingIconContent(content);
211 },
212 enumerable: false,
213 configurable: true
214 });
215 Object.defineProperty(MDCSelect.prototype, "helperTextContent", {
216 /**
217 * Sets the text content of the helper text.
218 */
219 set: function (content) {
220 this.foundation.setHelperTextContent(content);
221 },
222 enumerable: false,
223 configurable: true
224 });
225 Object.defineProperty(MDCSelect.prototype, "useDefaultValidation", {
226 /**
227 * Enables or disables the default validation scheme where a required select
228 * must be non-empty. Set to false for custom validation.
229 * @param useDefaultValidation Set this to false to ignore default
230 * validation scheme.
231 */
232 set: function (useDefaultValidation) {
233 this.foundation.setUseDefaultValidation(useDefaultValidation);
234 },
235 enumerable: false,
236 configurable: true
237 });
238 Object.defineProperty(MDCSelect.prototype, "valid", {
239 /**
240 * Checks if the select is in a valid state.
241 */
242 get: function () {
243 return this.foundation.isValid();
244 },
245 /**
246 * Sets the current invalid state of the select.
247 */
248 set: function (isValid) {
249 this.foundation.setValid(isValid);
250 },
251 enumerable: false,
252 configurable: true
253 });
254 Object.defineProperty(MDCSelect.prototype, "required", {
255 /**
256 * Returns whether the select is required.
257 */
258 get: function () {
259 return this.foundation.getRequired();
260 },
261 /**
262 * Sets the control to the required state.
263 */
264 set: function (isRequired) {
265 this.foundation.setRequired(isRequired);
266 },
267 enumerable: false,
268 configurable: true
269 });
270 /**
271 * Re-calculates if the notched outline should be notched and if the label
272 * should float.
273 */
274 MDCSelect.prototype.layout = function () {
275 this.foundation.layout();
276 };
277 /**
278 * Synchronizes the list of options with the state of the foundation. Call
279 * this whenever menu options are dynamically updated.
280 */
281 MDCSelect.prototype.layoutOptions = function () {
282 this.foundation.layoutOptions();
283 this.menu.layout();
284 // Update cached menuItemValues for adapter.
285 this.menuItemValues =
286 this.menu.items.map(function (el) { return el.getAttribute(strings.VALUE_ATTR) || ''; });
287 if (this.hiddenInput) {
288 this.hiddenInput.value = this.value;
289 }
290 };
291 MDCSelect.prototype.getDefaultFoundation = function () {
292 // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
293 // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
294 var adapter = __assign(__assign(__assign(__assign({}, this.getSelectAdapterMethods()), this.getCommonAdapterMethods()), this.getOutlineAdapterMethods()), this.getLabelAdapterMethods());
295 return new MDCSelectFoundation(adapter, this.getFoundationMap());
296 };
297 /**
298 * Handles setup for the menu.
299 */
300 MDCSelect.prototype.menuSetup = function (menuFactory) {
301 this.menuElement = this.root.querySelector(strings.MENU_SELECTOR);
302 this.menu = menuFactory(this.menuElement);
303 this.menu.hasTypeahead = true;
304 this.menu.singleSelection = true;
305 this.menuItemValues =
306 this.menu.items.map(function (el) { return el.getAttribute(strings.VALUE_ATTR) || ''; });
307 };
308 MDCSelect.prototype.createRipple = function () {
309 var _this = this;
310 // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial<MDCFooAdapter>.
311 // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
312 // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
313 var adapter = __assign(__assign({}, MDCRipple.createAdapter({ root: this.selectAnchor })), { registerInteractionHandler: function (evtType, handler) {
314 _this.selectAnchor.addEventListener(evtType, handler);
315 }, deregisterInteractionHandler: function (evtType, handler) {
316 _this.selectAnchor.removeEventListener(evtType, handler);
317 } });
318 // tslint:enable:object-literal-sort-keys
319 return new MDCRipple(this.selectAnchor, new MDCRippleFoundation(adapter));
320 };
321 MDCSelect.prototype.getSelectAdapterMethods = function () {
322 var _this = this;
323 // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
324 return {
325 getMenuItemAttr: function (menuItem, attr) {
326 return menuItem.getAttribute(attr);
327 },
328 setSelectedText: function (text) {
329 _this.selectedText.textContent = text;
330 },
331 isSelectAnchorFocused: function () { return document.activeElement === _this.selectAnchor; },
332 getSelectAnchorAttr: function (attr) {
333 return _this.selectAnchor.getAttribute(attr);
334 },
335 setSelectAnchorAttr: function (attr, value) {
336 _this.selectAnchor.setAttribute(attr, value);
337 },
338 removeSelectAnchorAttr: function (attr) {
339 _this.selectAnchor.removeAttribute(attr);
340 },
341 addMenuClass: function (className) {
342 _this.menuElement.classList.add(className);
343 },
344 removeMenuClass: function (className) {
345 _this.menuElement.classList.remove(className);
346 },
347 openMenu: function () {
348 _this.menu.open = true;
349 },
350 closeMenu: function () {
351 _this.menu.open = false;
352 },
353 getAnchorElement: function () {
354 return _this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR);
355 },
356 setMenuAnchorElement: function (anchorEl) {
357 _this.menu.setAnchorElement(anchorEl);
358 },
359 setMenuAnchorCorner: function (anchorCorner) {
360 _this.menu.setAnchorCorner(anchorCorner);
361 },
362 setMenuWrapFocus: function (wrapFocus) {
363 _this.menu.wrapFocus = wrapFocus;
364 },
365 getSelectedIndex: function () {
366 var index = _this.menu.selectedIndex;
367 return index instanceof Array ? index[0] : index;
368 },
369 setSelectedIndex: function (index) {
370 _this.menu.selectedIndex = index;
371 },
372 focusMenuItemAtIndex: function (index) {
373 _this.menu.items[index].focus();
374 },
375 getMenuItemCount: function () { return _this.menu.items.length; },
376 // Cache menu item values. layoutOptions() updates this cache.
377 getMenuItemValues: function () { return _this.menuItemValues; },
378 getMenuItemTextAtIndex: function (index) {
379 return _this.menu.getPrimaryTextAtIndex(index);
380 },
381 isTypeaheadInProgress: function () { return _this.menu.typeaheadInProgress; },
382 typeaheadMatchItem: function (nextChar, startingIndex) {
383 return _this.menu.typeaheadMatchItem(nextChar, startingIndex);
384 },
385 };
386 // tslint:enable:object-literal-sort-keys
387 };
388 MDCSelect.prototype.getCommonAdapterMethods = function () {
389 var _this = this;
390 // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
391 return {
392 addClass: function (className) {
393 _this.root.classList.add(className);
394 },
395 removeClass: function (className) {
396 _this.root.classList.remove(className);
397 },
398 hasClass: function (className) { return _this.root.classList.contains(className); },
399 setRippleCenter: function (normalizedX) {
400 _this.lineRipple && _this.lineRipple.setRippleCenter(normalizedX);
401 },
402 activateBottomLine: function () {
403 _this.lineRipple && _this.lineRipple.activate();
404 },
405 deactivateBottomLine: function () {
406 _this.lineRipple && _this.lineRipple.deactivate();
407 },
408 notifyChange: function (value) {
409 if (_this.hiddenInput) {
410 _this.hiddenInput.value = value;
411 }
412 var index = _this.selectedIndex;
413 _this.emit(strings.CHANGE_EVENT, { value: value, index: index }, true /* shouldBubble */);
414 },
415 };
416 // tslint:enable:object-literal-sort-keys
417 };
418 MDCSelect.prototype.getOutlineAdapterMethods = function () {
419 var _this = this;
420 // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
421 return {
422 hasOutline: function () { return Boolean(_this.outline); },
423 notchOutline: function (labelWidth) {
424 _this.outline && _this.outline.notch(labelWidth);
425 },
426 closeOutline: function () {
427 _this.outline && _this.outline.closeNotch();
428 },
429 };
430 // tslint:enable:object-literal-sort-keys
431 };
432 MDCSelect.prototype.getLabelAdapterMethods = function () {
433 var _this = this;
434 // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
435 return {
436 hasLabel: function () { return !!_this.label; },
437 floatLabel: function (shouldFloat) {
438 _this.label && _this.label.float(shouldFloat);
439 },
440 getLabelWidth: function () { return _this.label ? _this.label.getWidth() : 0; },
441 setLabelRequired: function (isRequired) {
442 _this.label && _this.label.setRequired(isRequired);
443 },
444 };
445 // tslint:enable:object-literal-sort-keys
446 };
447 /**
448 * Calculates where the line ripple should start based on the x coordinate within the component.
449 */
450 MDCSelect.prototype.getNormalizedXCoordinate = function (evt) {
451 var targetClientRect = evt.target.getBoundingClientRect();
452 var xCoordinate = this.isTouchEvent(evt) ? evt.touches[0].clientX : evt.clientX;
453 return xCoordinate - targetClientRect.left;
454 };
455 MDCSelect.prototype.isTouchEvent = function (evt) {
456 return Boolean(evt.touches);
457 };
458 /**
459 * Returns a map of all subcomponents to subfoundations.
460 */
461 MDCSelect.prototype.getFoundationMap = function () {
462 return {
463 helperText: this.helperText ? this.helperText.foundationForSelect :
464 undefined,
465 leadingIcon: this.leadingIcon ? this.leadingIcon.foundationForSelect :
466 undefined,
467 };
468 };
469 return MDCSelect;
470}(MDCComponent));
471export { MDCSelect };
472//# sourceMappingURL=component.js.map
\No newline at end of file