input-plus-minus
Version:
wrapper around input
503 lines (459 loc) • 14.7 kB
text/typescript
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;