UNPKG

md2

Version:

Angular2 based Material Design components, directives and services are Accordion, Autocomplete, Chips(Tags), Collapse, Colorpicker, Data Table, Datepicker, Dialog(Modal), Menu, Multiselect, Select, Tabs, Tags(Chips), Toast and Tooltip.

1,000 lines 47.5 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import { Component, ContentChildren, ElementRef, EventEmitter, Input, Optional, Output, QueryList, Renderer2, Self, ViewEncapsulation, ViewChild, ChangeDetectorRef, Attribute, } from '@angular/core'; import { Md2Option } from './option'; import { Md2Optgroup } from './optgroup'; import { ENTER, SPACE, UP_ARROW, DOWN_ARROW, HOME, END } from '../core/keyboard/keycodes'; import { FocusKeyManager } from '../core/a11y/focus-key-manager'; import { Dir } from '../core/rtl/dir'; import { Observable } from 'rxjs/Observable'; import { transformPlaceholder, transformPanel, fadeInContent } from './select-animations'; import { NgControl } from '@angular/forms'; import { coerceBooleanProperty } from '../core/coercion/boolean-property'; import { ConnectedOverlayDirective } from '../core/overlay/overlay-directives'; import { ViewportRuler } from '../core/overlay/position/viewport-ruler'; import { SelectionModel } from '../core/selection/selection'; import { getMdSelectDynamicMultipleError, getMdSelectNonArrayValueError } from './select-errors'; import 'rxjs/add/observable/merge'; import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/filter'; /** * The following style constants are necessary to save here in order * to properly calculate the alignment of the selected option over * the trigger element. */ /** The fixed height of every option element (option, group header etc.). */ export var SELECT_ITEM_HEIGHT = 48; /** The max height of the select's overlay panel */ export var SELECT_PANEL_MAX_HEIGHT = 256; /** The max number of options visible at once in the select panel. */ export var SELECT_MAX_OPTIONS_DISPLAYED = Math.floor(SELECT_PANEL_MAX_HEIGHT / SELECT_ITEM_HEIGHT); /** The fixed height of the select's trigger element. */ export var SELECT_TRIGGER_HEIGHT = 30; /** * Must adjust for the difference in height between the option and the trigger, * so the text will align on the y axis. */ export var SELECT_ITEM_HEIGHT_ADJUSTMENT = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2; /** The panel's padding on the x-axis */ export var SELECT_PANEL_PADDING_X = 16; /** The panel's x axis padding if it is indented (e.g. there is an option group). */ export var SELECT_PANEL_INDENT_PADDING_X = SELECT_PANEL_PADDING_X * 2; /** * Distance between the panel edge and the option text in * multi-selection mode. * * (SELECT_PADDING * 1.75) + 20 = 48 * The padding is multiplied by 1.75 because the checkbox's margin is half the padding, and * the browser adds ~4px, because we're using inline elements. * The checkbox width is 20px. */ export var SELECT_MULTIPLE_PANEL_PADDING_X = SELECT_PANEL_PADDING_X * 1.25 + 20; /** * The panel's padding on the y-axis. This padding indicates there are more * options available if you scroll. */ export var SELECT_PANEL_PADDING_Y = 16; /** * The select panel will only "fit" inside the viewport if it is positioned at * this value or more away from the viewport boundary. */ export var SELECT_PANEL_VIEWPORT_PADDING = 8; /** Change event object that is emitted when the select value has changed. */ var Md2SelectChange = (function () { function Md2SelectChange(source, value) { this.source = source; this.value = value; } return Md2SelectChange; }()); export { Md2SelectChange }; var Md2Select = (function () { function Md2Select(_element, _renderer, _viewportRuler, _changeDetectorRef, _dir, _control, tabIndex) { this._element = _element; this._renderer = _renderer; this._viewportRuler = _viewportRuler; this._changeDetectorRef = _changeDetectorRef; this._dir = _dir; this._control = _control; /** Whether or not the overlay panel is open. */ this._panelOpen = false; /** Whether filling out the select is required in the form. */ this._required = false; /** Whether the select is disabled. */ this._disabled = false; /** The scroll position of the overlay panel, calculated to center the selected option. */ this._scrollTop = 0; /** Whether the component is in multiple selection mode. */ this._multiple = false; /** The animation state of the placeholder. */ this._placeholderState = ''; /** View -> model callback called when value changes */ this._onChange = function () { }; /** View -> model callback called when select has been touched */ this._onTouched = function () { }; /** The IDs of child options to be passed to the aria-owns attribute. */ this._optionIds = ''; /** The value of the select panel's transform-origin property. */ this._transformOrigin = 'top'; /** Whether the panel's animation is done. */ this._panelDoneAnimating = false; /** * The y-offset of the overlay panel in relation to the trigger's top start corner. * This must be adjusted to align the selected option text over the trigger text. * when the panel opens. Will change based on the y-position of the selected option. */ this._offsetY = 0; /** * This position config ensures that the top "start" corner of the overlay * is aligned with with the top "start" of the origin by default (overlapping * the trigger completely). If the panel cannot fit below the trigger, it * will fall back to a position above the trigger. */ this._positions = [ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'top', }, { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'bottom', }, ]; this._floatPlaceholder = 'auto'; /** Aria label of the select. If not specified, the placeholder will be used as label. */ this.ariaLabel = ''; /** Input that can be used to specify the `aria-labelledby` attribute. */ this.ariaLabelledby = ''; /** Event emitted when the select has been opened. */ this.onOpen = new EventEmitter(); /** Event emitted when the select has been closed. */ this.onClose = new EventEmitter(); /** Event emitted when the selected value has been changed by the user. */ this.change = new EventEmitter(); if (this._control) { this._control.valueAccessor = this; } this._tabIndex = parseInt(tabIndex) || 0; } Object.defineProperty(Md2Select.prototype, "placeholder", { /** Placeholder to be shown if no value has been selected. */ get: function () { return this._placeholder; }, set: function (value) { var _this = this; this._placeholder = value; // Must wait to record the trigger width to ensure placeholder width is included. Promise.resolve(null).then(function () { return _this._setTriggerWidth(); }); }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "disabled", { /** Whether the component is disabled. */ get: function () { return this._disabled; }, set: function (value) { this._disabled = coerceBooleanProperty(value); }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "required", { /** Whether the component is required. */ get: function () { return this._required; }, set: function (value) { this._required = coerceBooleanProperty(value); }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "multiple", { /** Whether the user should be allowed to select multiple options. */ get: function () { return this._multiple; }, set: function (value) { if (this._selectionModel) { throw getMdSelectDynamicMultipleError(); } this._multiple = coerceBooleanProperty(value); }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "floatPlaceholder", { /** Whether to float the placeholder text. */ get: function () { return this._floatPlaceholder; }, set: function (value) { this._floatPlaceholder = value || 'auto'; }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "tabIndex", { /** Tab index for the select element. */ get: function () { return this._disabled ? -1 : this._tabIndex; }, set: function (value) { if (typeof value !== 'undefined') { this._tabIndex = value; } }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "optionSelectionChanges", { /** Combined stream of all of the child options' change events. */ get: function () { return Observable.merge.apply(Observable, this.options.map(function (option) { return option.onSelectionChange; })); }, enumerable: true, configurable: true }); Md2Select.prototype.ngOnInit = function () { this._selectionModel = new SelectionModel(this.multiple, null, false); }; Md2Select.prototype.ngAfterContentInit = function () { var _this = this; this._initKeyManager(); this._changeSubscription = this.options.changes.startWith(null).subscribe(function () { _this._resetOptions(); if (_this._control) { // Defer setting the value in order to avoid the "Expression // has changed after it was checked" errors from Angular. Promise.resolve(null).then(function () { return _this._setSelectionByValue(_this._control.value); }); } }); }; Md2Select.prototype.ngOnDestroy = function () { this._dropSubscriptions(); if (this._changeSubscription) { this._changeSubscription.unsubscribe(); } if (this._tabSubscription) { this._tabSubscription.unsubscribe(); } }; /** Toggles the overlay panel open or closed. */ Md2Select.prototype.toggle = function () { this.panelOpen ? this.close() : this.open(); }; /** Opens the overlay panel. */ Md2Select.prototype.open = function () { if (this.disabled || !this.options.length) { return; } if (!this._triggerWidth) { this._setTriggerWidth(); } this._calculateOverlayPosition(); this._placeholderState = this._floatPlaceholderState(); this._panelOpen = true; }; /** Closes the overlay panel and focuses the host element. */ Md2Select.prototype.close = function () { if (this._panelOpen) { this._panelOpen = false; if (this._selectionModel.isEmpty()) { this._placeholderState = ''; } this._focusHost(); } }; /** * Sets the select's value. Part of the ControlValueAccessor interface * required to integrate with Angular's core forms API. * * @param value New value to be written to the model. */ Md2Select.prototype.writeValue = function (value) { if (this.options) { this._setSelectionByValue(value); } }; /** * Saves a callback function to be invoked when the select's value * changes from user input. Part of the ControlValueAccessor interface * required to integrate with Angular's core forms API. * * @param fn Callback to be triggered when the value changes. */ Md2Select.prototype.registerOnChange = function (fn) { this._onChange = fn; }; /** * Saves a callback function to be invoked when the select is blurred * by the user. Part of the ControlValueAccessor interface required * to integrate with Angular's core forms API. * * @param fn Callback to be triggered when the component has been touched. */ Md2Select.prototype.registerOnTouched = function (fn) { this._onTouched = fn; }; /** * Disables the select. Part of the ControlValueAccessor interface required * to integrate with Angular's core forms API. * * @param isDisabled Sets whether the component is disabled. */ Md2Select.prototype.setDisabledState = function (isDisabled) { this.disabled = isDisabled; }; Object.defineProperty(Md2Select.prototype, "panelOpen", { /** Whether or not the overlay panel is open. */ get: function () { return this._panelOpen; }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "selected", { /** The currently selected option. */ get: function () { return this.multiple ? this._selectionModel.selected : this._selectionModel.selected[0]; }, enumerable: true, configurable: true }); Object.defineProperty(Md2Select.prototype, "triggerValue", { /** The value displayed in the trigger. */ get: function () { if (this._multiple) { var selectedOptions = this._selectionModel.selected.map(function (option) { return option.viewValue; }); if (this._isRtl()) { selectedOptions.reverse(); } // TODO(crisbeto): delimiter should be configurable for proper localization. return selectedOptions.join(', '); } return this._selectionModel.selected[0].viewValue; }, enumerable: true, configurable: true }); /** Whether the element is in RTL mode. */ Md2Select.prototype._isRtl = function () { return this._dir ? this._dir.value === 'rtl' : false; }; /** * Sets the width of the trigger element. This is necessary to match * the overlay width to the trigger width. */ Md2Select.prototype._setTriggerWidth = function () { this._triggerWidth = this._getTriggerRect().width; }; /** Handles the keyboard interactions of a closed select. */ Md2Select.prototype._handleClosedKeydown = function (event) { if (!this.disabled) { if (event.keyCode === ENTER || event.keyCode === SPACE) { event.preventDefault(); // prevents the page from scrolling down when pressing space this.open(); } else if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) { this._handleArrowKey(event); } } }; /** Handles keypresses inside the panel. */ Md2Select.prototype._handlePanelKeydown = function (event) { if (event.keyCode === HOME || event.keyCode === END) { event.preventDefault(); event.keyCode === HOME ? this._keyManager.setFirstItemActive() : this._keyManager.setLastItemActive(); } else { this._keyManager.onKeydown(event); } }; /** * When the panel element is finished transforming in (though not fading in), it * emits an event and focuses an option if the panel is open. */ Md2Select.prototype._onPanelDone = function () { if (this.panelOpen) { this._focusCorrectOption(); this.onOpen.emit(); } else { this.onClose.emit(); this._panelDoneAnimating = false; this.overlayDir.offsetX = 0; } }; /** * When the panel content is done fading in, the _panelDoneAnimating property is * set so the proper class can be added to the panel. */ Md2Select.prototype._onFadeInDone = function () { this._panelDoneAnimating = this.panelOpen; }; /** * Calls the touched callback only if the panel is closed. Otherwise, the trigger will * "blur" to the panel when it opens, causing a false positive. */ Md2Select.prototype._onBlur = function () { if (!this.panelOpen) { this._onTouched(); } }; /** * Callback that is invoked when the overlay panel has been attached. */ Md2Select.prototype._onAttached = function () { this._calculateOverlayOffsetX(); this._setScrollTop(); }; /** * Sets the scroll position of the scroll container. This must be called after * the overlay pane is attached or the scroll container element will not yet be * present in the DOM. */ Md2Select.prototype._setScrollTop = function () { var scrollContainer = this.overlayDir.overlayRef.overlayElement.querySelector('.md2-select-panel'); scrollContainer.scrollTop = this._scrollTop; }; /** * Sets the selected option based on a value. If no option can be * found with the designated value, the select trigger is cleared. */ Md2Select.prototype._setSelectionByValue = function (value) { var _this = this; var isArray = Array.isArray(value); if (this.multiple && value && !isArray) { throw getMdSelectNonArrayValueError(); } this._clearSelection(); if (isArray) { value.forEach(function (currentValue) { return _this._selectValue(currentValue); }); this._sortValues(); } else { this._selectValue(value); } this._setValueWidth(); if (this._selectionModel.isEmpty()) { this._placeholderState = ''; } this._changeDetectorRef.markForCheck(); }; /** * Finds and selects and option based on its value. * @returns Option that has the corresponding value. */ Md2Select.prototype._selectValue = function (value) { var _this = this; var optionsArray = this.options.toArray(); var correspondingOption = optionsArray.find(function (option) { return option.value != null && option.value != undefined && _this.equals(option.value, value); }); if (correspondingOption) { correspondingOption.select(); this._selectionModel.select(correspondingOption); this._keyManager.setActiveItem(optionsArray.indexOf(correspondingOption)); } return correspondingOption; }; /** * Compare two vars or objects * @param o1 compare first object * @param o2 compare second object * @return boolean comparation result */ Md2Select.prototype.equals = function (o1, o2) { if (o1 === o2) { return true; } if (o1 === null || o2 === null) { return false; } if (o1 !== o1 && o2 !== o2) { return true; } var t1 = typeof o1, t2 = typeof o2, key, keySet; if (t1 === t2 && t1 === 'object') { keySet = Object.create(null); for (key in o1) { if (!this.equals(o1[key], o2[key])) { return false; } keySet[key] = true; } for (key in o2) { if (!(key in keySet) && key.charAt(0) !== '$' && o2[key]) { return false; } } return true; } return false; }; /** * Clears the select trigger and deselects every option in the list. * @param skip Option that should not be deselected. */ Md2Select.prototype._clearSelection = function (skip) { this._selectionModel.clear(); this.options.forEach(function (option) { if (option !== skip) { option.deselect(); } }); }; Md2Select.prototype._getTriggerRect = function () { return this.trigger.nativeElement.getBoundingClientRect(); }; /** Sets up a key manager to listen to keyboard events on the overlay panel. */ Md2Select.prototype._initKeyManager = function () { var _this = this; this._keyManager = new FocusKeyManager(this.options); this._tabSubscription = this._keyManager.tabOut.subscribe(function () { return _this.close(); }); }; /** Drops current option subscriptions and IDs and resets from scratch. */ Md2Select.prototype._resetOptions = function () { this._dropSubscriptions(); this._listenToOptions(); this._setOptionIds(); this._setOptionMultiple(); }; /** Listens to user-generated selection events on each option. */ Md2Select.prototype._listenToOptions = function () { var _this = this; this._optionSubscription = this.optionSelectionChanges .filter(function (event) { return event.isUserInput; }) .subscribe(function (event) { _this._onSelect(event.source); _this._setValueWidth(); if (!_this.multiple) { _this.close(); } }); }; /** Invoked when an option is clicked. */ Md2Select.prototype._onSelect = function (option) { var wasSelected = this._selectionModel.isSelected(option); if (this.multiple) { this._selectionModel.toggle(option); wasSelected ? option.deselect() : option.select(); this._sortValues(); } else { this._clearSelection(option.value == null ? null : option); if (option.value == null) { this._propagateChanges(option.value); } else { this._selectionModel.select(option); } } if (wasSelected !== this._selectionModel.isSelected(option)) { this._propagateChanges(); } }; /** * Sorts the model values, ensuring that they keep the same * order that they have in the panel. */ Md2Select.prototype._sortValues = function () { var _this = this; if (this._multiple) { this._selectionModel.clear(); this.options.forEach(function (option) { if (option.selected) { _this._selectionModel.select(option); } }); } }; /** Unsubscribes from all option subscriptions. */ Md2Select.prototype._dropSubscriptions = function () { if (this._optionSubscription) { this._optionSubscription.unsubscribe(); this._optionSubscription = null; } }; /** Emits change event to set the model value. */ Md2Select.prototype._propagateChanges = function (fallbackValue) { var valueToEmit = null; if (Array.isArray(this.selected)) { valueToEmit = this.selected.map(function (option) { return option.value; }); } else { valueToEmit = this.selected ? this.selected.value : fallbackValue; } this._onChange(valueToEmit); this.change.emit(new Md2SelectChange(this, valueToEmit)); }; /** Records option IDs to pass to the aria-owns property. */ Md2Select.prototype._setOptionIds = function () { this._optionIds = this.options.map(function (option) { return option.id; }).join(' '); }; /** * Sets the `multiple` property on each option. The promise is necessary * in order to avoid Angular errors when modifying the property after init. */ Md2Select.prototype._setOptionMultiple = function () { var _this = this; if (this.multiple) { Promise.resolve(null).then(function () { _this.options.forEach(function (option) { return option.multiple = _this.multiple; }); }); } }; /** * Must set the width of the selected option's value programmatically * because it is absolutely positioned and otherwise will not clip * overflow. The selection arrow is 9px wide, add 4px of padding = 13 */ Md2Select.prototype._setValueWidth = function () { this._selectedValueWidth = this._triggerWidth - 13; }; /** * Focuses the selected item. If no option is selected, it will focus * the first item instead. */ Md2Select.prototype._focusCorrectOption = function () { if (this._selectionModel.isEmpty()) { this._keyManager.setFirstItemActive(); } else { this._keyManager.setActiveItem(this._getOptionIndex(this._selectionModel.selected[0])); } }; /** Focuses the host element when the panel closes. */ Md2Select.prototype._focusHost = function () { this._element.nativeElement.focus(); }; /** Gets the index of the provided option in the option list. */ Md2Select.prototype._getOptionIndex = function (option) { return this.options.reduce(function (result, current, index) { return result === undefined ? (option === current ? index : undefined) : result; }, undefined); }; /** Calculates the scroll position and x- and y-offsets of the overlay panel. */ Md2Select.prototype._calculateOverlayPosition = function () { var items = this._getItemCount(); var panelHeight = Math.min(items * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT); var scrollContainerHeight = items * SELECT_ITEM_HEIGHT; // The farthest the panel can be scrolled before it hits the bottom var maxScroll = scrollContainerHeight - panelHeight; if (this._selectionModel.hasValue()) { var selectedIndex = this._getOptionIndex(this._selectionModel.selected[0]); selectedIndex += this._getLabelCountBeforeOption(selectedIndex); // We must maintain a scroll buffer so the selected option will be scrolled to the // center of the overlay panel rather than the top. var scrollBuffer = panelHeight / 2; this._scrollTop = this._calculateOverlayScroll(selectedIndex, scrollBuffer, maxScroll); this._offsetY = this._calculateOverlayOffsetY(selectedIndex, scrollBuffer, maxScroll); } else { // If no option is selected, the panel centers on the first option. In this case, // we must only adjust for the height difference between the option element // and the trigger element, then multiply it by -1 to ensure the panel moves // in the correct direction up the page. this._offsetY = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2 * -1; } this._checkOverlayWithinViewport(maxScroll); }; /** * Calculates the scroll position of the select's overlay panel. * * Attempts to center the selected option in the panel. If the option is * too high or too low in the panel to be scrolled to the center, it clamps the * scroll position to the min or max scroll positions respectively. */ Md2Select.prototype._calculateOverlayScroll = function (selectedIndex, scrollBuffer, maxScroll) { var optionOffsetFromScrollTop = SELECT_ITEM_HEIGHT * selectedIndex; var halfOptionHeight = SELECT_ITEM_HEIGHT / 2; // Starts at the optionOffsetFromScrollTop, which scrolls the option to the top of the // scroll container, then subtracts the scroll buffer to scroll the option down to // the center of the overlay panel. Half the option height must be re-added to the // scrollTop so the option is centered based on its middle, not its top edge. var optimalScrollPosition = optionOffsetFromScrollTop - scrollBuffer + halfOptionHeight; return clampValue(0, optimalScrollPosition, maxScroll); }; /** * Figures out the appropriate animation state for the placeholder. */ Md2Select.prototype._getPlaceholderAnimationState = function () { if (this.floatPlaceholder === 'never') { return ''; } if (this.floatPlaceholder === 'always') { return this._floatPlaceholderState(); } return this._placeholderState; }; /** * Determines the CSS `opacity` of the placeholder element. */ Md2Select.prototype._getPlaceholderOpacity = function () { return (this.floatPlaceholder !== 'never' || this._selectionModel.isEmpty()) ? '1' : '0'; }; Object.defineProperty(Md2Select.prototype, "_ariaLabel", { /** Returns the aria-label of the select component. */ get: function () { // If an ariaLabelledby value has been set, the select should not overwrite the // `aria-labelledby` value by setting the ariaLabel to the placeholder. return this.ariaLabelledby ? null : this.ariaLabel || this.placeholder; }, enumerable: true, configurable: true }); /** * Sets the x-offset of the overlay panel in relation to the trigger's top start corner. * This must be adjusted to align the selected option text over the trigger text when * the panel opens. Will change based on LTR or RTL text direction. Note that the offset * can't be calculated until the panel has been attached, because we need to know the * content width in order to constrain the panel within the viewport. */ Md2Select.prototype._calculateOverlayOffsetX = function () { var overlayRect = this.overlayDir.overlayRef.overlayElement.getBoundingClientRect(); var viewportRect = this._viewportRuler.getViewportRect(); var isRtl = this._isRtl(); var offsetX; // Adjust the offset, depending on the option padding. if (this.multiple) { offsetX = SELECT_MULTIPLE_PANEL_PADDING_X; } else { var selected = this._selectionModel.selected[0]; offsetX = selected && selected.group ? SELECT_PANEL_INDENT_PADDING_X : SELECT_PANEL_PADDING_X; } // Invert the offset in LTR. if (!isRtl) { offsetX *= -1; } // Determine how much the select overflows on each side. var leftOverflow = 0 - (overlayRect.left + offsetX - (isRtl ? SELECT_PANEL_PADDING_X * 2 : 0)); var rightOverflow = overlayRect.right + offsetX - viewportRect.width + (isRtl ? 0 : SELECT_PANEL_PADDING_X * 2); // If the element overflows on either side, reduce the offset to allow it to fit. if (leftOverflow > 0) { offsetX += leftOverflow + SELECT_PANEL_VIEWPORT_PADDING; } else if (rightOverflow > 0) { offsetX -= rightOverflow + SELECT_PANEL_VIEWPORT_PADDING; } // Set the offset directly in order to avoid having to go through change detection and // potentially triggering "changed after it was checked" errors. this.overlayDir.offsetX = offsetX; this.overlayDir.overlayRef.updatePosition(); }; /** * Calculates the y-offset of the select's overlay panel in relation to the * top start corner of the trigger. It has to be adjusted in order for the * selected option to be aligned over the trigger when the panel opens. */ Md2Select.prototype._calculateOverlayOffsetY = function (selectedIndex, scrollBuffer, maxScroll) { var optionOffsetFromPanelTop; if (this._scrollTop === 0) { optionOffsetFromPanelTop = selectedIndex * SELECT_ITEM_HEIGHT; } else if (this._scrollTop === maxScroll) { var firstDisplayedIndex = this._getItemCount() - SELECT_MAX_OPTIONS_DISPLAYED; var selectedDisplayIndex = selectedIndex - firstDisplayedIndex; // Because the panel height is longer than the height of the options alone, // there is always extra padding at the top or bottom of the panel. When // scrolled to the very bottom, this padding is at the top of the panel and // must be added to the offset. optionOffsetFromPanelTop = selectedDisplayIndex * SELECT_ITEM_HEIGHT + SELECT_PANEL_PADDING_Y; } else { // If the option was scrolled to the middle of the panel using a scroll buffer, // its offset will be the scroll buffer minus the half height that was added to // center it. optionOffsetFromPanelTop = scrollBuffer - SELECT_ITEM_HEIGHT / 2; } // The final offset is the option's offset from the top, adjusted for the height // difference, multiplied by -1 to ensure that the overlay moves in the correct // direction up the page. return optionOffsetFromPanelTop * -1 - SELECT_ITEM_HEIGHT_ADJUSTMENT; }; /** * Checks that the attempted overlay position will fit within the viewport. * If it will not fit, tries to adjust the scroll position and the associated * y-offset so the panel can open fully on-screen. If it still won't fit, * sets the offset back to 0 to allow the fallback position to take over. */ Md2Select.prototype._checkOverlayWithinViewport = function (maxScroll) { var viewportRect = this._viewportRuler.getViewportRect(); var triggerRect = this._getTriggerRect(); var topSpaceAvailable = triggerRect.top - SELECT_PANEL_VIEWPORT_PADDING; var bottomSpaceAvailable = viewportRect.height - triggerRect.bottom - SELECT_PANEL_VIEWPORT_PADDING; var panelHeightTop = Math.abs(this._offsetY); var totalPanelHeight = Math.min(this._getItemCount() * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT); var panelHeightBottom = totalPanelHeight - panelHeightTop - triggerRect.height; if (panelHeightBottom > bottomSpaceAvailable) { this._adjustPanelUp(panelHeightBottom, bottomSpaceAvailable); } else if (panelHeightTop > topSpaceAvailable) { this._adjustPanelDown(panelHeightTop, topSpaceAvailable, maxScroll); } else { this._transformOrigin = this._getOriginBasedOnOption(); } }; /** Adjusts the overlay panel up to fit in the viewport. */ Md2Select.prototype._adjustPanelUp = function (panelHeightBottom, bottomSpaceAvailable) { var distanceBelowViewport = panelHeightBottom - bottomSpaceAvailable; // Scrolls the panel up by the distance it was extending past the boundary, then // adjusts the offset by that amount to move the panel up into the viewport. this._scrollTop -= distanceBelowViewport; this._offsetY -= distanceBelowViewport; this._transformOrigin = this._getOriginBasedOnOption(); // If the panel is scrolled to the very top, it won't be able to fit the panel // by scrolling, so set the offset to 0 to allow the fallback position to take // effect. if (this._scrollTop <= 0) { this._scrollTop = 0; this._offsetY = 0; this._transformOrigin = "50% bottom 0px"; } }; /** Adjusts the overlay panel down to fit in the viewport. */ Md2Select.prototype._adjustPanelDown = function (panelHeightTop, topSpaceAvailable, maxScroll) { var distanceAboveViewport = panelHeightTop - topSpaceAvailable; // Scrolls the panel down by the distance it was extending past the boundary, then // adjusts the offset by that amount to move the panel down into the viewport. this._scrollTop += distanceAboveViewport; this._offsetY += distanceAboveViewport; this._transformOrigin = this._getOriginBasedOnOption(); // If the panel is scrolled to the very bottom, it won't be able to fit the // panel by scrolling, so set the offset to 0 to allow the fallback position // to take effect. if (this._scrollTop >= maxScroll) { this._scrollTop = maxScroll; this._offsetY = 0; this._transformOrigin = "50% top 0px"; return; } }; /** Sets the transform origin point based on the selected option. */ Md2Select.prototype._getOriginBasedOnOption = function () { var originY = Math.abs(this._offsetY) - SELECT_ITEM_HEIGHT_ADJUSTMENT + SELECT_ITEM_HEIGHT / 2; return "50% " + originY + "px 0px"; }; /** Figures out the floating placeholder state value. */ Md2Select.prototype._floatPlaceholderState = function () { return this._isRtl() ? 'floating-rtl' : 'floating-ltr'; }; /** Handles the user pressing the arrow keys on a closed select. */ Md2Select.prototype._handleArrowKey = function (event) { if (this._multiple) { event.preventDefault(); this.open(); } else { var prevActiveItem = this._keyManager.activeItem; // Cycle though the select options even when the select is closed, // matching the behavior of the native select element. // TODO(crisbeto): native selects also cycle through the options with left/right arrows, // however the key manager only supports up/down at the moment. this._keyManager.onKeydown(event); var currentActiveItem = this._keyManager.activeItem; if (currentActiveItem !== prevActiveItem) { this._clearSelection(); this._setSelectionByValue(currentActiveItem.value); this._propagateChanges(); } } }; /** Calculates the amount of items in the select. This includes options and group labels. */ Md2Select.prototype._getItemCount = function () { return this.options.length + this.optionGroups.length; }; /** * Calculates the amount of option group labels that precede the specified option. * Useful when positioning the panel, because the labels will offset the index of the * currently-selected option. */ Md2Select.prototype._getLabelCountBeforeOption = function (optionIndex) { if (this.optionGroups.length) { var options = this.options.toArray(); var groups = this.optionGroups.toArray(); var groupCounter = 0; for (var i = 0; i < optionIndex + 1; i++) { if (options[i].group && options[i].group === groups[groupCounter]) { groupCounter++; } } return groupCounter; } return 0; }; return Md2Select; }()); __decorate([ ViewChild('trigger'), __metadata("design:type", ElementRef) ], Md2Select.prototype, "trigger", void 0); __decorate([ ViewChild(ConnectedOverlayDirective), __metadata("design:type", ConnectedOverlayDirective) ], Md2Select.prototype, "overlayDir", void 0); __decorate([ ContentChildren(Md2Option, { descendants: true }), __metadata("design:type", QueryList) ], Md2Select.prototype, "options", void 0); __decorate([ ContentChildren(Md2Optgroup), __metadata("design:type", QueryList) ], Md2Select.prototype, "optionGroups", void 0); __decorate([ Input(), __metadata("design:type", Object), __metadata("design:paramtypes", [String]) ], Md2Select.prototype, "placeholder", null); __decorate([ Input(), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], Md2Select.prototype, "disabled", null); __decorate([ Input(), __metadata("design:type", Object), __metadata("design:paramtypes", [Object]) ], Md2Select.prototype, "required", null); __decorate([ Input(), __metadata("design:type", Boolean), __metadata("design:paramtypes", [Boolean]) ], Md2Select.prototype, "multiple", null); __decorate([ Input(), __metadata("design:type", String), __metadata("design:paramtypes", [String]) ], Md2Select.prototype, "floatPlaceholder", null); __decorate([ Input(), __metadata("design:type", Number), __metadata("design:paramtypes", [Number]) ], Md2Select.prototype, "tabIndex", null); __decorate([ Input('aria-label'), __metadata("design:type", String) ], Md2Select.prototype, "ariaLabel", void 0); __decorate([ Input('aria-labelledby'), __metadata("design:type", String) ], Md2Select.prototype, "ariaLabelledby", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], Md2Select.prototype, "onOpen", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], Md2Select.prototype, "onClose", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], Md2Select.prototype, "change", void 0); Md2Select = __decorate([ Component({selector: 'md2-select', template: "<div class=\"md2-select-trigger\" cdk-overlay-origin (click)=\"toggle()\" #origin=\"cdkOverlayOrigin\" #trigger><span class=\"md2-select-placeholder\" [class.md2-floating-placeholder]=\"_selectionModel.hasValue()\" [@transformPlaceholder]=\"_getPlaceholderAnimationState()\" [style.opacity]=\"_getPlaceholderOpacity()\" [style.width.px]=\"_selectedValueWidth\">{{ placeholder }}</span> <span class=\"md2-select-value\" *ngIf=\"_selectionModel.hasValue()\"><span class=\"md2-select-value-text\">{{ triggerValue }}</span> </span><span class=\"md2-select-arrow\"></span> <span class=\"md2-select-underline\"></span></div><ng-template cdk-connected-overlay [origin]=\"origin\" [open]=\"panelOpen\" hasBackdrop (backdropClick)=\"close()\" backdropClass=\"cdk-overlay-transparent-backdrop\" [positions]=\"_positions\" [minWidth]=\"_triggerWidth\" [offsetY]=\"_offsetY\" (attach)=\"_onAttached()\" (detach)=\"close()\"><div class=\"md2-select-panel\" [@transformPanel]=\"'showing'\" (@transformPanel.done)=\"_onPanelDone()\" (keydown)=\"_keyManager.onKeydown($event)\" [style.transformOrigin]=\"_transformOrigin\" [class.md2-select-panel-done-animating]=\"_panelDoneAnimating\"><div class=\"md2-select-content\" [@fadeInContent]=\"'showing'\" (@fadeInContent.done)=\"_onFadeInDone()\"><ng-content select=\"md2-select-header\"></ng-content><ng-content></ng-content></div></div></ng-template>", styles: [".md2-select{display:inline-block;outline:0}.md2-select-trigger{color:rgba(0,0,0,.38);display:flex;align-items:center;height:30px;min-width:112px;cursor:pointer;position:relative;box-sizing:border-box;font-size:16px}[aria-disabled=true] .md2-select-trigger{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.md2-select:focus:not(.md2-select-disabled) .md2-select-trigger{color:#106cc8}.md2-select.ng-invalid.ng-touched:not(.md2-select-disabled) .md2-select-trigger{color:#f44336}.md2-select-underline{position:absolute;bottom:0;left:0;right:0;height:1px;background-color:rgba(0,0,0,.12)}[aria-disabled=true] .md2-select-underline{background-image:linear-gradient(to right,rgba(0,0,0,.26) 0,rgba(0,0,0,.26) 33%,transparent 0);background-size:4px 1px;background-repeat:repeat-x;background-color:transparent;background-position:0 bottom}.md2-select:focus:not(.md2-select-disabled) .md2-select-underline{background-color:#106cc8}.md2-select.ng-invalid.ng-touched:not(.md2-select-disabled) .md2-select-underline{background-color:#f44336}.md2-select-placeholder{position:relative;padding:0 2px;transform-origin:left top;flex-grow:1}.md2-select-placeholder.md2-floating-placeholder{top:-22px;left:-2px;text-align:left;transform:scale(.75)}[dir=rtl] .md2-select-placeholder{transform-origin:right top}[dir=rtl] .md2-select-placeholder.md2-floating-placeholder{left:2px;text-align:right}[aria-required=true] .md2-select-placeholder::after{content:'*';color:#fd0f0f}.md2-select-value{position:absolute;max-width:calc(100% - 18px);flex-grow:1;top:0;left:0;bottom:0;display:flex;align-items:center;color:rgba(0,0,0,.87)}[dir=rtl] .md2-select-value{left:auto;right:0}.md2-select-disabled .md2-select-value{color:rgba(0,0,0,.38)}.md2-select-value-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:30px}.md2-select-arrow{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid;margin:0 4px}.md2-select-panel{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);min-width:112px;max-width:280px;overflow:auto;-webkit-overflow-scrolling:touch;padding-top:0;padding-bottom:0;max-height:256px;min-width:100%}@media screen and (-ms-high-contrast:active){.md2-select-panel{outline:solid 1px}}.md2-select-content,.md2-select-panel-done-animating{background:#fff}.cdk-global-overlay-wrapper,.cdk-overlay-container{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:999}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:999}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;z-index:999;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-transparent-backdrop{background:0 0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48} /*# sourceMappingURL=select.css.map */ "], encapsulation: ViewEncapsulation.None, host: { 'role': 'listbox', '[attr.tabindex]': 'tabIndex', '[attr.aria-label]': '_ariaLabel', '[attr.aria-labelledby]': 'ariaLabelledby', '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-invalid]': '_control?.invalid || "false"', '[attr.aria-owns]': '_optionIds', '[class.md2-select-disabled]': 'disabled', '[class.md2-select]': 'true', '(keydown)': '_handleClosedKeydown($event)', '(blur)': '_onBlur()', }, animations: [ transformPlaceholder, transformPanel, fadeInContent ], exportAs: 'md2Select', }), __param(4, Optional()), __param(5, Self()), __param(5, Optional()), __param(6, Attribute('tabindex')), __metadata("design:paramtypes", [ElementRef, Renderer2, ViewportRuler, ChangeDetectorRef, Dir, NgControl, String]) ], Md2Select); export { Md2Select }; /** Clamps a value n between min and max values. */ function clampValue(min, n, max) { return Math.min(Math.max(min, n), max); } //# sourceMappingURL=select.js.map