UNPKG

input-plus-minus

Version:
503 lines (459 loc) 14.7 kB
import { addClassesThemes, changeTextContentGridElement, checkElementIsset, checkNumber, checkStringOnFloat, createChanger, createGridElement, createGridWrapper, createTypicalEvent, formatGridElementText, getMinBorderFromSteps, getNextValue, getNextValueByObjectStep, getPrevValue, getPrevValueByObjectStep, issetObject, occurrenceNumberInSection, parseStrToNumber, prepareInitElement, removeClassesThemes, thumbCreatorDefault, wrapInput } from './functions'; import './style/InputPlusMinus.scss'; import { InputPlusMinusElements, InputPlusMinusEventData, InputPlusMinusEvents, InputPlusMinusSettings } from './interfaces'; import { createCustomEvent, createObjectEventAfterChange, createObjectEventBeforeChange } from './events'; import Inputmask from 'inputmask'; import Callbacks from './Callbacks'; import InputPlusMinusThemes from './InputPlusMinusThemes'; const CLASSES = { wrapper: 'InputPlusMinus', element: 'InputPlusMinus-Element', changer: 'InputPlusMinus-Changer', minus: 'InputPlusMinus-Minus', plus: 'InputPlusMinus-Plus', grid: 'InputPlusMinus-Grid', gridElement: 'InputPlusMinus-GridElement', gridElementMin: 'InputPlusMinus-GridElement_min', gridElementMax: 'InputPlusMinus-GridElement_max' }; const MAX_NUMBER = Number.MAX_SAFE_INTEGER; const MIN_NUMBER = Number.MIN_SAFE_INTEGER; class InputPlusMinus { public self: HTMLInputElement; public saveValidValue: number; protected lastInputValue: string; protected configuration: InputPlusMinusSettings; protected usedChanges: InputPlusMinusSettings; protected mask: Inputmask.Instance; public callbacks: Callbacks; protected themes: string[] = []; public static themes: InputPlusMinusThemes = new InputPlusMinusThemes(); protected static instances: WeakMap< HTMLInputElement, InputPlusMinus > = new WeakMap(); public elements: InputPlusMinusElements = { wrapper: null, minus: null, plus: null, grid: null, gridMin: null, gridMax: null, thumb: null }; public constructor( initElement: Element | string, settings?: InputPlusMinusSettings, themes?: string[] ) { const issetSettings = issetObject(settings); this.self = prepareInitElement(initElement) as HTMLInputElement; if (InputPlusMinus.checkInstance(this.self)) { throw new Error('This element has instance!'); } this.self.classList.add(CLASSES.element); this.elements.wrapper = wrapInput(this.self, CLASSES.wrapper); this.callbacks = new Callbacks(); if (issetSettings) { this.updateConfiguration(settings, themes, false, true); } else { this.updateConfiguration({}, themes, false, true); } this.elements.minus = createChanger(this.configuration.minusText, [ CLASSES.changer, CLASSES.minus ]); this.self.parentNode.insertBefore(this.elements.minus, this.self); this.elements.plus = createChanger(this.configuration.plusText, [ CLASSES.changer, CLASSES.plus ]); this.elements.wrapper.appendChild(this.elements.plus); this.updateStatusChangers(); this.addEventListeners(); InputPlusMinus.addInstanceToList(this); } protected addEventListeners(): void { this.self.addEventListener('input', this.handleInput); this.self.addEventListener('blur', this.handleBlur); this.elements.minus.addEventListener('click', this.handleMinusClick); this.elements.plus.addEventListener('click', this.handlePlusClick); } protected removeEventListeners(): void { this.self.removeEventListener('input', this.handleInput); this.self.removeEventListener('blur', this.handleBlur); this.elements.minus.addEventListener('click', this.handleMinusClick); this.elements.plus.addEventListener('click', this.handlePlusClick); } protected handleInput = (): void => { const value = this.self.value; this.lastInputValue = value; if (this.validateValue(value)) { this.onChange(value); } }; protected handleBlur = (): void => { const value = this.lastInputValue; const validValue = this.getValidValue(value).toString(); if (value !== validValue) { this.self.value = validValue; this.generateEvent('input'); } }; protected handleMinusClick = () => { this.prev(); }; protected handlePlusClick = () => { this.next(); }; protected onChange(value: string): void { this.generateEvent( 'beforeChange', createObjectEventBeforeChange( this, this.saveValidValue, parseStrToNumber(value) ) ); this.saveValidValue = parseStrToNumber(value); this.updateStatusChangers(); this.generateEvent( 'afterChange', createObjectEventAfterChange(this, this.saveValidValue) ); } public changeValue(value: number): void { const { min, max } = this.configuration; if (occurrenceNumberInSection(value, min, max)) { this.self.value = value.toString(); this.generateEvent('input'); return; } throw new Error(`Value "${value}" not allowed!`); } public updateConfiguration( settings: InputPlusMinusSettings, themes?: string[], fireInput: boolean = false, start: boolean = false ): void { let value = this.self.value; this.usedChanges = Object.assign( {}, InputPlusMinus.themes.getThemesObject(themes), settings ); this.configuration = Object.assign( {}, InputPlusMinus.defaultSettings(), this.usedChanges ); removeClassesThemes(this.elements.wrapper, CLASSES.wrapper, this.themes); this.themes = Array.isArray(themes) ? themes : []; addClassesThemes(this.elements.wrapper, CLASSES.wrapper, this.themes); const step = this.configuration.step; if (!checkNumber(step)) { const minFromSteps = getMinBorderFromSteps(step); const min = this.usedChanges.min; if (checkNumber(min) && minFromSteps > min) { this.configuration.min = getMinBorderFromSteps(step); } } if (checkNumber(settings.start)) { value = settings.start.toString(); } if (!checkStringOnFloat(value)) { value = '0'; } this.saveValidValue = this.getValidValue(value); this.updateMask({}); this.self.value = this.saveValidValue.toString(); this.createGrid(); this.createThumb(); if (!start) { this.updateStatusChangers(); } if (fireInput) { this.generateEvent('input'); } } public next(): void { const value = this.saveValidValue; const toValue = this.getStepNextValue(value); this.changeValue(toValue); } public prev(): void { const value = this.saveValidValue; const toValue = this.getStepPrevValue(value); this.changeValue(toValue); } protected validateValue(value: string): boolean { if (!this.mask.isValid()) { return false; } if (!checkStringOnFloat(value)) { return false; } const numb = parseStrToNumber(value); const { min, max } = this.configuration; return occurrenceNumberInSection(numb, min, max); } protected getValidValue(value: string): number { const { min, max } = this.configuration; const valueNumber = parseStrToNumber(value); if (isNaN(valueNumber)) { return this.saveValidValue; } if (valueNumber < min) { return min; } if (valueNumber > max) { return max; } return valueNumber; } protected getStepNextValue(current: number): number { const step = this.configuration.step; const max = this.configuration.max; if (checkNumber(step)) { return getNextValue(current, step, max); } return getNextValueByObjectStep(current, step, max); } protected getStepPrevValue(current: number): number { const step = this.configuration.step; const min = this.configuration.min; if (checkNumber(step)) { return getPrevValue(current, step, min); } return getPrevValueByObjectStep(current, step, min); } protected updateMask(options: Inputmask.Options): void { if (issetObject(this.mask) && this.mask instanceof Inputmask) { this.mask.remove(); } const configMask = Object.assign( {}, InputPlusMinus.defaultMaskSettings(), this.getMaskSettingsFromConfig(), options ); this.mask = Inputmask('numeric', configMask).mask(this.self); } protected getMaskSettingsFromConfig(): Inputmask.Options { const { digits, max, min } = this.configuration; return { digits: digits.toString(), max: max.toString(), min: min.toString(), allowMinus: min < 0 }; } protected updateStatusChangers(): void { const value = parseStrToNumber(this.self.value); const { min, max } = this.configuration; const { minus, plus } = this.elements; (minus as HTMLButtonElement).disabled = value <= min; (plus as HTMLButtonElement).disabled = value >= max; } protected generateEvent( type: InputPlusMinusEvents, data?: InputPlusMinusEventData ): void { let event; const self = this.self; switch (type) { case 'input': event = createTypicalEvent('input'); self.dispatchEvent(event); break; case 'beforeChange': case 'afterChange': this.callbacks.fireCallbacksByType(type, data); event = createCustomEvent(type, data); self.dispatchEvent(event); break; default: throw new Error(`Event with name: "${type}" can't generate`); } } protected getBorderValuesFromChanges(): { min: number; max: number } { const { min, max } = this.configuration; let minUse; let maxUse; if (min !== InputPlusMinus.defaultSettings().min) { minUse = min; } if (max !== InputPlusMinus.defaultSettings().max) { maxUse = max; } return { min: minUse, max: maxUse }; } protected createGrid(): void { this.removeGrid(); const { grid, gridSuffix } = this.configuration; const { min, max } = this.getBorderValuesFromChanges(); const issetMin = checkNumber(min); const issetMax = checkNumber(max); if (grid && (issetMax || issetMin)) { let gridWrapper = this.elements.grid; if (!checkElementIsset(gridWrapper)) { gridWrapper = createGridWrapper(this.elements.wrapper, [CLASSES.grid]); this.elements.grid = gridWrapper; } if (issetMin) { let gridMin = this.elements.gridMin; if (!checkElementIsset(gridMin)) { gridMin = createGridElement(gridWrapper, [ CLASSES.gridElement, CLASSES.gridElementMin ]); this.elements.gridMin = gridMin; } let minText = formatGridElementText( min, this.configuration.gridCompression, this.configuration.gridCompressionValues ); changeTextContentGridElement(gridMin, minText, gridSuffix); } if (issetMax) { let gridMax = this.elements.gridMax; if (!checkElementIsset(gridMax)) { gridMax = createGridElement(gridWrapper, [ CLASSES.gridElement, CLASSES.gridElementMax ]); this.elements.gridMax = gridMax; } let maxText = formatGridElementText( max, this.configuration.gridCompression, this.configuration.gridCompressionValues ); changeTextContentGridElement(gridMax, maxText, gridSuffix); } } } protected removeGrid(): void { const grid = this.elements.grid; if (checkElementIsset(grid)) { grid.parentNode.removeChild(grid); this.elements.grid = null; this.elements.gridMin = null; this.elements.gridMax = null; } } protected createThumb(): void { this.removeThumb(); const { thumb, thumbCreator } = this.configuration; if (typeof thumb === 'string') { this.elements.thumb = thumbCreator(thumb, this.self); } } protected removeThumb(): void { const { thumb } = this.elements; if (checkElementIsset(thumb)) { thumb.parentNode.removeChild(thumb); this.elements.thumb = null; } } public destructor(): void { const self = this.self; const parent = this.elements.wrapper.parentNode; this.callbacks.destructor(); self.classList.remove(CLASSES.element); this.removeEventListeners(); parent.appendChild(self); parent.removeChild(this.elements.wrapper); this.mask.remove(); this.removeThumb(); InputPlusMinus.removeInstanceFromList(self); } public static defaultMaskSettings(): Inputmask.Options { return { radixPoint: '.', digits: '2', integerDigits: '13', groupSeparator: ' ', autoGroup: true, rightAlign: false, autoUnmask: true }; } public static defaultSettings(): InputPlusMinusSettings { return { minusText: '−', plusText: '+', step: 1, min: MIN_NUMBER, max: MAX_NUMBER, digits: 2, grid: false, gridSuffix: '', gridCompression: true, gridCompressionValues: [ { text: '', compression: 0, digits: 0 }, { text: 'тыс.', compression: 3, digits: 0 }, { text: 'млн.', compression: 6, digits: 1 }, { text: 'млрд.', compression: 9, digits: 1 } ], thumbCreator: thumbCreatorDefault }; } public static getInstance(info: Element | string): InputPlusMinus { const instances = this.instances; const element = prepareInitElement(info) as HTMLInputElement; if (instances.has(element)) { return instances.get(element); } throw new Error(`Нет инициализированного элемента!`); } public static checkInstance(info: HTMLInputElement): boolean { return this.instances.has(info); } protected static addInstanceToList(instanceElement: InputPlusMinus): void { this.instances.set(instanceElement.self, instanceElement); } protected static removeInstanceFromList( instanceElement: HTMLInputElement ): void { this.instances.delete(instanceElement); } } export default InputPlusMinus;