import {Component, ElementRef, HostListener, Input} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {AbstractComponent, HelperService} from 'gp-admin-abstract'; @Component({ selector: 'gp-spinbox', template: `
{{ title }}
{{ error }}
`, styles: [ `@charset "UTF-8"; /** * Переменные */ :root { --color-white: white; --color-success: #00afec; --color-warning: #ffb827; --color-error: #f35252; --color-disabled: #dbdbdb; --color-black: black; --color-span: #b5b5b5; --color-span-second: #212121; --color-bg: #f8f8f8; --color-bg-second: #f8f8f8; --color-bg-success: rgba(0, 175, 236, 0.1); --color-bg-warning: var(--color-warning); --color-bg-error: var(--color-error); --color-placeholder: #b5b5b5; --color-border: #dbdbdb; --color-radio-border: #d3d3d3; --font-roboto: 'Roboto', sans-serif; --font-helvetica: 'Helvetica', sans-serif; } /** * */ /** * Стили для компонента "gp-spinbox" */ .gp-spinbox { position: relative; } .gp-spinbox > .pr:after { position: absolute; z-index: 0; top: 0.2rem; right: 0.2rem; bottom: 0.2rem; display: block; width: 2.3rem; content: ''; border-radius: 0.3rem; background-color: var(--color-white); } .gp-spinbox__plus, .gp-spinbox__minus { position: absolute; width: 0.6rem; height: 0.4rem; cursor: pointer; z-index: 1; } .gp-spinbox__plus { top: 0.2rem; right: 0.2rem; padding: 0.8rem 1.3rem; } .gp-spinbox__plus:before { content: ''; position: absolute; display: block; border-right: 0.3rem solid transparent; border-bottom: 0.4rem solid #dbdbdb; border-left: 0.3rem solid transparent; top: 1rem; right: 1rem; } .gp-spinbox__plus:hover:before { border-bottom-color: #b5b5b5; } .gp-spinbox__minus { right: 0.2rem; bottom: 0.2rem; padding: 0.8rem 1.3rem; } .gp-spinbox__minus:hover:before { border-top-color: #b5b5b5; } .gp-spinbox__minus:before { content: ''; position: absolute; display: block; border-top: 0.4rem solid #dbdbdb; border-right: 0.3rem solid transparent; border-left: 0.3rem solid transparent; right: 1rem; bottom: 1rem; } .gp-spinbox--success input.gp-spinbox__area { border-color: #00afec; } .gp-spinbox--success .gp-spinbox__title { color: #00afec; } .gp-spinbox--error input.gp-spinbox__area { border-color: #f35252; } .gp-spinbox--error .gp-spinbox__title { color: #f35252; } .gp-spinbox--error .gp-spinbox__error { display: block; } .gp-spinbox--disabled .gp-spinbox__minus:hover, .gp-spinbox--disabled .gp-spinbox__plus:hover { cursor: not-allowed; } .gp-spinbox--disabled .gp-spinbox__minus:hover:before, .gp-spinbox--disabled .gp-spinbox__plus:hover:before { border-top-color: #dbdbdb; border-bottom-color: #dbdbdb; } .gp-spinbox--disabled > .pr:after { background-color: #f8f8f8; cursor: not-allowed; } .gp-spinbox--disabled input.gp-spinbox__area { cursor: not-allowed; color: #b5b5b5; border-color: #dbdbdb; background-color: #f8f8f8; } .gp-spinbox__area { font: 1.3rem/1.5rem "Roboto", sans-serif; min-width: 18rem; width: 100%; padding: 1rem 1rem 1rem 1.5rem; color: #212121; border: 0.1rem solid #dbdbdb; border-radius: 0.2rem; background-color: var(--color-white); } .gp-spinbox__title { font: 500 1.2rem/1.4rem "Roboto", sans-serif; margin-bottom: 0.5rem; color: #b5b5b5; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .gp-spinbox__error { font: 500 1.2rem/1.4rem "Roboto", sans-serif; display: none; color: #f35252; } input::-webkit-input-placeholder, input:-ms-input-placeholder, input::placeholder { color: #b5b5b5; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } `], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: SpinboxComponent, multi: true } ] }) export class SpinboxComponent extends AbstractComponent implements ControlValueAccessor { private static readonly KEY_CODE_UP = 38; private static readonly KEY_CODE_DOWN = 40; private static readonly ALLOW_KEYS = [46, 8, 9, 27, 13, 110, 190]; /** * Заголовок компонента */ @Input() title: string; /** * Подсказка до ввода * * @type {string} */ @Input() placeholder: string; /** * Шаг, на который будет изменяться знаечние * * @type {number} */ @Input() step: number = 1; /** * Минимальное значение * * @type {number} */ @Input() minValue: number | boolean = false; /** * Максимальное значение * * @type {number} */ @Input() maxValue: number | boolean = false; /** * Текущее значение * * @type {number} */ @Input() value: number = +this.minValue || 0; /** * Состояние доступности компонента * * @type {boolean} */ @Input() disabled = false; /** * Текст ошибки * * @type {string | boolean} */ @Input() error: string | boolean = false; // TODO: когда появится верстка, и появится ли вообще @Input() prefix: string | boolean = false; @Input() prefixIcon: string | boolean = false; @Input() postfix: string | boolean = false; @Input() postfixIcon: string | boolean = false; // TODO: --------------------------------------------- constructor(protected el: ElementRef, private helperService: HelperService) { super(el); } @HostListener('keydown', ['$event']) onKeyDown(event: any) { const e = event; if (SpinboxComponent.ALLOW_KEYS.indexOf(e.keyCode) !== -1 || // Allow: Ctrl+A (e.keyCode === 65 && e.ctrlKey === true) || // Allow: Ctrl+C (e.keyCode === 67 && e.ctrlKey === true) || // Allow: Ctrl+V (e.keyCode === 86 && e.ctrlKey === true) || // Allow: Ctrl+X (e.keyCode === 88 && e.ctrlKey === true) || // Allow: home, end, left, right (e.keyCode >= 35 && e.keyCode <= 39)) { // let it happen, don't do anything return; } // Ensure that it is a number and stop the keypress if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) { e.preventDefault(); } } writeValue(value: number): void { if (this.validateValue(value)) { this.value = value; } } registerOnChange(fn: any): void { this.propagateChange = fn; } propagateChange = (_: any) => { }; registerOnTouched(fn: any): void { } setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } /** * При вводе нажатии клавиши внутри html-элемента input * * @param {KeyboardEvent} event */ onInputKeyDown(event: KeyboardEvent) { const keyCode = event.which || event.keyCode; if (keyCode === SpinboxComponent.KEY_CODE_DOWN) { event.preventDefault(); this.clickArrowDown(); } else if (keyCode === SpinboxComponent.KEY_CODE_UP) { event.preventDefault(); this.clickArrowUp(); } } /** * Проверяем и запрещаем вводить символов больше, чем указано * Если вводят больше, чем можно, устанавливаем максимальное * * @param {KeyboardEvent} event */ onKeyPress(event: KeyboardEvent): void { if (this.value && (String(this.value).length === String(this.maxValue).length) && (!(!!this.helperService.getSelectedText()))) { event.preventDefault(); if (this.value < +this.maxValue) { this.value = +this.maxValue; } } } /** * При вводе данных */ change(): void { this.propagateChange(this.value); } showErrorText(): boolean { return ((typeof this.error === 'string') && !!this.error); } /** * Клик по стрелочке 'вверх' */ clickArrowUp() { const newVal = +this.value + this.step; if (this.maxValue === false || newVal <= this.maxValue) { this.value = newVal; this.propagateChange(newVal); } } /** * Клик по стрелочке 'вниз' */ clickArrowDown() { const newVal = +this.value - this.step; if (this.minValue === false || newVal >= this.minValue) { this.value = newVal; this.propagateChange(newVal); } } /** * При потере фокуса * выставлять в поле минимальное значение */ inputBlur(): void { this.checkValue(); } private validateValue(value: number): boolean { return ((this.maxValue === false) || (value <= this.maxValue)) && ((this.minValue === false) || (value >= this.minValue)); } /** * При вставке значений в компонент * * Нужна небольшая задержка, для получения данных после вставки */ onPaste(): void { setTimeout(() => { this.checkValue(); }, 1); } /** * Проверка значения при вставке | вводе с клавиатуры */ checkValue() { this.value = Number(this.value); if (String(this.value).length === 0) { this.value = +this.minValue; } if (String(this.value).length > String(this.maxValue).length) { this.value = +this.maxValue; } } }