component.js 21 KB

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