UNPKG

stf-angular-select

Version:

Custom reusable select for angular with wide posibilities

531 lines (425 loc) 19.5 kB
import * as angular from 'angular'; import { IOptionScope } from './stf-select-option.directive'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/throttleTime'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/observable/fromEvent'; import * as _ from "lodash"; export interface IScopeStfSelect extends angular.IScope { label: string; ngModel: any; focused: boolean; fixNgModal: boolean; selectId: number; ngDisabled: boolean, disabled: boolean, optionsClass: string; } export class StfSelectDirective { _getScope: () => IScopeStfSelect = () => null; template: string = ` <section class="stf-select" ng-class="{'stf-select_has-value': ngModel? true : false, 'stf-select_focused': focused, 'stf-select_disabled': disabled || ngDisabled}"> <div class="stf-select__label" ng-transclude="label"></div> <section class="stf-select__container" > <section class="stf-select__inner-wrapper"> <div class="stf-select__value" ng-transclude="value"></div> <div class="stf-select__placeholder" ng-transclude="label"></div> <div class="stf-select__icon"></div> </section> <section class="stf-select__search-input" tabindex="{{ngDisabled ? '-1' : '0'}}" ng-transclude="searchInput"></section> <section class="stf-select__options"> <div class="stf-select__fixed-option sss"></div> </section> </section> </section> `; //ng-transclude="options" restrict: string = 'E'; require = "ngModel"; scope: any = { fixNgModal: "<", ngDisabled: "<", disabled: "<", optionsClass: "<" }; transclude: any = { label: 'stfSelectLabel', value: 'stfSelectValue', //options: 'stfSelectOptions', searchInput: '?stfSearchInput', //fixedOption: '?stfFixedOption' }; constructor(protected $translate: angular.translate.ITranslateService, protected $window: angular.IWindowService, protected $compile: angular.ICompileService, protected _NP_STF_SELECT_THROTTLE_TIME: number) { } link(scope: IScopeStfSelect, element, attributes: any, ngModelController: angular.INgModelController, transcludeFn: angular.ITranscludeFunction) { if (scope.fixNgModal) mdFixes(); element.addClass("stf-select-root"); let $mainContainer = $(window); let openScrollTimerId; let jqSelectOptions = element.find('.stf-select__options'); let placeHolder = element.find('.stf-select__placeholder'); let self = this; let elementChildren = element.children('.stf-select'); let valueClicked = false; render(); ngModelController.$render = render; scope.selectId = Math.round(Math.random() * 100000000000); element.attr('data-stf-select-id', scope.selectId); const $body = $('body'); const $searchInputContainer = element.find('.stf-select__search-input'); const elemetClickSubscription = Observable.fromEvent($searchInputContainer, 'click') .subscribe((event: any) => { event.stopPropagation(); }); $searchInputContainer.delegate("input", "keydown", function (event) { let inputs = $searchInputContainer.find('input'); let keyCode = event.keyCode || event.which; if(inputs.length > 1){ if(event.target !== inputs[inputs.length - 1] && keyCode === 9){ event.stopPropagation(); } } }); const $searchInputContainerKeyDownSubscription = Observable.fromEvent($searchInputContainer, 'keydown') .subscribe( (event: any) => { var keyCode = event.keyCode || event.which; if (scope.focused) { switch (keyCode) { case 9: hideDropDown(); break; case 40: selectKeyDownPressed(); break; case 38: selectKeyUpPressed(); break; case 27: $searchInputContainer.focus(); hideDropDown(); event.preventDefault(); event.stopPropagation(); break; case 13: if(keyCode === 13){ event.stopPropagation(); let optionForClick = $(`#stf-select-optins-${scope.selectId} .stf-select-option__selected`); if(!optionForClick.length){ optionForClick = $(`#stf-select-optins-${scope.selectId} .stf-select-option`).first(); } optionForClick.click(); return; } break; } } else { switch (keyCode) { case 13: showDropDown(); break; } } } ); const $searchInputFocusSubscription = Observable.fromEvent($searchInputContainer, 'focus') .subscribe((event: any) => { elementChildren.addClass('stf-select__tab-focus'); }); const $searchInputBlurSubscription = Observable.fromEvent($searchInputContainer, 'blur') .subscribe((event: any) => { elementChildren.removeClass('stf-select__tab-focus'); }); const $searchInputKePressSubscription = Observable.fromEvent($searchInputContainer, 'click') .subscribe((event: any) => { showDropDown(); }); const valueContainer = element.find('.stf-select__inner-wrapper'); const valueContainerObservable: Observable<any> = Observable.fromEvent(valueContainer, 'click').throttleTime(this._NP_STF_SELECT_THROTTLE_TIME); const valueContainerSubscription = valueContainerObservable.subscribe(event => { if (!scope.disabled && !scope.ngDisabled) { scope.focused = true; scope.$apply(); } }); const iconEl = element.find('.stf-select__icon'); const iconElObservable: Observable<any> = Observable.fromEvent(iconEl, 'click').throttleTime(this._NP_STF_SELECT_THROTTLE_TIME); const iconElSubscription = iconElObservable.subscribe(event => { if (scope.focused) { event.stopPropagation(); scope.focused = false; scope.$apply(); } }); const windowClickObservable: Observable<any> = Observable.fromEvent(this.$window, 'click').throttleTime(100); const windowClickSubscription = windowClickObservable.subscribe((event) => { if(!$(event.target).closest(`[data-stf-select-id=${scope.selectId}]`).length){ scope.focused = false; scope.$applyAsync(); } }); const windowResizeObservable: Observable<any> = Observable.fromEvent(this.$window, 'resize').throttleTime(100); const windowResizeSubscription = windowResizeObservable.subscribe( event => calculatePositionAnsSize() ); let scrollListener = _.debounce(() => { calculatePositionAnsSize(); }, 100); document.addEventListener('scroll', scrollListener, true); const elementMouseWheelObservable = Observable.fromEvent(element, "mousewheel"); const elementMouseWheelSubscription = elementMouseWheelObservable.subscribe((event: any) => { if (scope.focused) { event.stopPropagation(); event.returnValue = false; } }); const stfSelectOption = element.find('.stf-select-option'); scope.$on('stf-select-option.selected', (event, value: any) => { let old = ngModelController.$viewValue; scope.focused = false; if (old !== value) { ngModelController.$setViewValue(value); scope.ngModel = value; scope.$broadcast('stf-select.value_changed', value); } setTimeout(() => $searchInputContainer.focus(), 100); } ); scope.$watch('focused', (newValue: boolean, oldValue) => { if (scope.focused) { if (element.find('.stf-select__search-input input').length) { setTimeout(() => scope.focused && element.find('.stf-select__search-input input').first().focus(), 200); } else if ($searchInputContainer.length) { setTimeout(() => scope.focused && $searchInputContainer.first().focus(), 200); } scrollUnscrollContainers(); calculatePositionAnsSize(); } else { scrollUnscrollContainers(); } }); let options; let fixedOpt; let jOptins; let jOptinsParent; let transcludedScope: angular.IScope; transcludeFn((transcludeEls, scopeTr: angular.IScope) => { _.each(transcludeEls, el => { if (el.tagName === "STF-SELECT-OPTIONS") { options = el; } if (el.tagName === "STF-FIXED-OPTION") { fixedOpt = el; } }); transcludedScope = scopeTr; $('body').append(this.$compile(` <div ng-attr-class="{{optionsClass}}"> <div ng-class="{'stf-select_has-value': ngModel? true : false, 'stf-select_focused': focused, 'stf-select_disabled': disabled || ngDisabled}"> <div class="stf-select__options" id="stf-select-optins-${scope.selectId}"></div> </div> </div> `)(scope)); jOptins = $(`#stf-select-optins-${scope.selectId}`); jOptins.append(options); jOptins.append('<div tabindex="0" class="stf-select__fixed-option"></div>'); jOptins.children('.stf-select__fixed-option').append(fixedOpt); jOptinsParent = jOptins.parent(); jOptins.delegate('.stf-select-option', "keydown", (function (event) { var keyCode = event.keyCode || event.which; switch (keyCode) { case 13: setTimeout(() => this.click(), 100); break; case 9: event.preventDefault(); event.stopPropagation(); element.find('.stf-select__search-input').focus(); hideDropDown(); break; case 40: { optionKeyDown($(event.target)); event.preventDefault(); } break; case 38: { optionKeyUp($(event.target)); event.preventDefault(); } break; case 27: element.find('.stf-select__search-input').focus(); hideDropDown(); event.preventDefault(); event.stopPropagation(); break; } })); jOptins.find('.stf-select__fixed-option').keydown(function (event) { var keyCode = event.keyCode || event.which; switch (keyCode) { case 13: { jOptins.find('stf-fixed-option').click(); hideDropDown(); } break; case 38: fixedKeyUpPressed(); break; case 40: fixedKeyDownPressed(); break; case 9: hideDropDown(); break; case 27: hideDropDown(); event.preventDefault(); event.stopPropagation(); break; } }); }); scope.$on('$destroy', () => { valueContainerSubscription.unsubscribe(); windowClickSubscription.unsubscribe(); windowResizeSubscription.unsubscribe(); jOptinsParent.parent().remove(); elementMouseWheelSubscription.unsubscribe(); elemetClickSubscription.unsubscribe(); iconElSubscription.unsubscribe(); document.removeEventListener('scroll', scrollListener, true); $('body, .modal-content').css('overflow-y', ''); clearTimeout(openScrollTimerId); $searchInputFocusSubscription.unsubscribe(); $searchInputKePressSubscription.unsubscribe(); transcludedScope.$destroy(); }); //calculatePositionAnsSize(); function calculatePositionAnsSize() { if (!scope.focused) { return; } let elOffset = element.children().offset(); jOptinsParent.width(elementChildren.width()); jOptins.width(elementChildren.width()); console.log($mainContainer.scrollTop()); if ( (jqSelectOptions.offset().top - $mainContainer.scrollTop() + jOptins.height() + 10 ) > (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) ) { jOptinsParent.addClass('top'); jOptins.css('top', elOffset.top - jOptins.height() - 28); } else { jOptinsParent.removeClass('top'); jOptins.css('top', jqSelectOptions.offset().top); } jOptins.css('left', elOffset.left); } function mdFixes() { } function scrollUnscrollContainers() { if (scope.focused) { let jqContainer = $('body, .modal-content'); jqContainer.css('overflow-y', 'hidden'); openScrollTimerId = setTimeout(() => jqContainer.css('overflow-y', 'hidden'), 200); } else { $('body, .modal-content').css('overflow-y', ''); } } element function showDropDown() { if (!scope.disabled && !scope.ngDisabled) { valueClicked = true; scope.focused = true; scope.$apply(); } } function hideDropDown() { scope.focused = false; jOptinsParent.width(0); setTimeout(() => { scope.focused || scope.$applyAsync(); }, 0); } function selectKeyDownPressed() { let elementForFocus = jOptins.find('.stf-select-option').first(); if(!elementForFocus.length){ elementForFocus = jOptins.find('.stf-select__fixed-option'); } setTimeout(() => elementForFocus.focus(), 100); } function selectKeyUpPressed() { let elementForFocus = jOptins.find('.stf-select__fixed-option').last(); if(!elementForFocus.children().length) elementForFocus.length = 0; if (!elementForFocus.length) { elementForFocus = jOptins.find('.stf-select-option').last(); } setTimeout(() => elementForFocus.focus(), 100); } function fixedKeyUpPressed() { let elementForFocus = jOptins.find('.stf-select-option').last(); if (!elementForFocus.length) { elementForFocus = $searchInputContainer.find('input').length ? $searchInputContainer.find('input').first() : $searchInputContainer; } setTimeout(() => elementForFocus.focus(), 100); } function fixedKeyDownPressed() { let elementForFocus = $searchInputContainer.find('input').length ? $searchInputContainer.find('input').first() : jOptins.find('.stf-select-option').first(); setTimeout(() => elementForFocus.focus(), 100); } function optionKeyDown(element) { let elementParentFocus = element.parent().next(); let elementForFocus = elementParentFocus.children(); if (!elementForFocus.length) { elementParentFocus = elementParentFocus.next(); elementForFocus = elementParentFocus.children(); } if (!elementForFocus.length) { let fixed = jOptins.find('.stf-select__fixed-option'); if(fixed.children().length) elementForFocus = jOptins.find('.stf-select__fixed-option'); } if (!elementForFocus.length) { elementForFocus = $searchInputContainer.find('input').first(); } if (!elementForFocus.length) { elementForFocus = jOptins.find('.stf-select-option').first(); } elementForFocus.focus(); } function optionKeyUp(element) { let elementParentFocus = element.parent().prev(); let elementForFocus = elementParentFocus.children(); if (!elementForFocus.length) { elementParentFocus = elementParentFocus.prev(); elementForFocus = elementParentFocus.children(); } if (!elementForFocus.length) { elementForFocus = $searchInputContainer.find('input').first(); } if (!elementForFocus.length) { let fixed = jOptins.find('.stf-select__fixed-option'); if(fixed.children().length) elementForFocus = jOptins.find('.stf-select__fixed-option'); } if (!elementForFocus.length) { elementForFocus = jOptins.find('.stf-select-option').last(); } elementForFocus.focus(); } function render(){ scope.ngModel = ngModelController.$viewValue; scope.$broadcast('stf-select.value_changed', scope.ngModel); return scope.ngModel; } } public static Factory($translate: angular.translate.ITranslateService, $window: angular.IWindowService, $compile: angular.ICompileService, NP_STF_SELECT_THROTTLE_TIME: number) { return new StfSelectDirective($translate, $window, $compile, NP_STF_SELECT_THROTTLE_TIME); } } StfSelectDirective.Factory.$inject = ['$translate', "$window", "$compile", "NP_STF_SELECT_THROTTLE_TIME"];