rabbit-simple-ui
Version:
A simple UI component library based on JavaScript
500 lines (443 loc) • 17.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
$el,
bind,
getBooleanTypeAttr,
getNumTypeAttr,
getStrTypeAttr,
removeAttrs,
setHtml
} from '../../dom-utils';
import { warn } from '../../mixins';
import { type, validComps } from '../../utils';
import PREFIX from '../prefix';
interface Config {
config(
el: string
): {
value: number;
step: number;
disabled: boolean;
readOnly: boolean;
editable: boolean;
events({ onChange, onFocus, onBlur }: InputNumberEvents): void;
};
}
interface InputNumberEvents {
onChange?: (value: number) => void;
onFocus?: (event: InputEvent) => void;
onBlur?: () => void;
}
function addNum(num1: number, num2: number): number {
let sq1: number, sq2: number;
try {
sq1 = num1.toString().split('.')[1].length;
} catch (e) {
sq1 = 0;
}
try {
sq2 = num2.toString().split('.')[1].length;
} catch (e) {
sq2 = 0;
}
const m = Math.pow(10, Math.max(sq1, sq2));
return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
}
class InputNumber implements Config {
readonly VERSION: string;
readonly COMPONENTS: NodeListOf<HTMLElement>;
constructor() {
this.VERSION = 'v1.0';
this.COMPONENTS = $el('r-input-number', { all: true });
this._create(this.COMPONENTS);
}
public config(
el: string
): {
value: number;
step: number;
disabled: boolean;
readOnly: boolean;
editable: boolean;
events({ onChange, onFocus, onBlur }: InputNumberEvents): void;
} {
const target = $el(el) as HTMLElement;
validComps(target, 'input-number');
const { _attrs, _setValue, _setDisabled } = InputNumber.prototype;
const { min, max, step, disabled, readOnly, editable, precision, formatter } = _attrs(
target
);
const Input = target.querySelector(`.${PREFIX.inputnb}-input`)! as HTMLInputElement;
const ArrowUp = target.querySelector(`.${PREFIX.inputnb}-handler-up`);
const ArrowDown = target.querySelector(`.${PREFIX.inputnb}-handler-down`);
const BtnUp = target.querySelector(`.${PREFIX.inputnb}-controls-outside-up`);
const BtnDown = target.querySelector(`.${PREFIX.inputnb}-controls-outside-down`);
return {
get value() {
return Number(Input.value);
},
set value(newVal: number) {
if (newVal && !type.isNum(newVal)) return;
_setValue(Input, newVal, formatter, precision, min, max);
},
get step() {
return step;
},
set step(newVal: number) {
if (newVal && !type.isNum(newVal)) return;
Input.step = step;
},
get disabled() {
return disabled;
},
set disabled(newVal: boolean) {
if (newVal && !type.isBol(newVal)) return;
_setDisabled(target, Input, newVal);
},
get readOnly() {
return readOnly;
},
set readOnly(newVal: boolean) {
if (newVal && !type.isBol(newVal)) return;
Input.readOnly = newVal;
const disableArrow = (elem1: Element | null, elem2: Element | null) => {
if (elem1) {
// @ts-ignore
elem1.style.pointerEvents = newVal ? 'none' : '';
// @ts-ignore
elem2.style.pointerEvents = newVal ? 'none' : '';
}
};
disableArrow(ArrowUp, ArrowDown);
disableArrow(BtnUp, BtnDown);
},
get editable() {
return editable;
},
set editable(newVal: boolean) {
if (newVal && !type.isBol(newVal)) return;
Input.style.pointerEvents = !newVal ? 'none' : '';
},
events({ onChange, onFocus, onBlur }) {
let value: number;
const changeEv = (e: Event) => {
e.stopPropagation();
value = Number(Input.value);
if (!isNaN(value)) {
onChange && type.isFn(onChange, value);
} else {
warn(`Invalid input value --> '${Input.value}' at '${el}'`);
return;
}
};
if (ArrowUp) {
bind(ArrowUp, 'click', changeEv);
bind(ArrowDown, 'click', changeEv);
}
if (BtnUp) {
bind(BtnUp, 'click', changeEv);
bind(BtnDown, 'click', changeEv);
}
bind(Input, 'keydown', (e: KeyboardEvent) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') changeEv(e);
});
bind(Input, 'input', (e: InputEvent) => changeEv(e));
bind(Input, 'focus', (e: InputEvent) => {
e.stopPropagation();
onFocus && type.isFn(onFocus, e);
});
bind(Input, 'blur', (e: InputEvent) => {
e.stopPropagation();
onBlur && type.isFn(onBlur);
});
}
};
}
private _create(COMPONENTS: NodeListOf<HTMLElement>) {
COMPONENTS.forEach((node) => {
const {
min,
max,
step,
value,
name,
inputId,
parser,
formatter,
precision,
disabled,
editable,
readOnly,
size,
placeholder,
controlsOutside
} = this._attrs(node);
this._setMainTemplate(node);
this._setOutSide(node, controlsOutside);
const Input = node.querySelector(`.${PREFIX.inputnb}-input`)! as HTMLInputElement;
const ArrowUp = node.querySelector(`.${PREFIX.inputnb}-handler-up`);
const ArrowDown = node.querySelector(`.${PREFIX.inputnb}-handler-down`);
const BtnUp = node.querySelector(`.${PREFIX.inputnb}-controls-outside-up`);
const BtnDown = node.querySelector(`.${PREFIX.inputnb}-controls-outside-down`);
this._setInput(Input, min, max, step, name, inputId, placeholder);
this._setValue(Input, value, formatter, precision, min, max);
this._setSize(node, size);
this._setDisabled(node, Input, disabled);
this._setReadonlyAndEditable(Input, readOnly, editable);
this._setHandler(ArrowUp, ArrowDown, BtnUp, BtnDown, value, min, max);
this._handleChange(
Input,
ArrowUp,
ArrowDown,
BtnUp,
BtnDown,
min,
max,
step,
precision,
readOnly,
parser,
formatter
);
removeAttrs(node, [
'min',
'max',
'step',
'value',
'precision',
'size',
'name',
'parser',
'formatter',
'input-id',
'placeholder',
'disabled',
'editable',
'readOnly',
'controls-outside'
]);
});
}
private _setMainTemplate(node: HTMLElement): void {
node.classList.add(`${PREFIX.inputnb}`);
const template = `
<div class="${PREFIX.inputnb}-handler-wrap">
<a class="${PREFIX.inputnb}-handler ${PREFIX.inputnb}-handler-up">
<span class="${PREFIX.inputnb}-handler-up-inner ${PREFIX.icon} ${PREFIX.icon}-ios-arrow-up"></span>
</a>
<a class="${PREFIX.inputnb}-handler ${PREFIX.inputnb}-handler-down">
<span class="${PREFIX.inputnb}-handler-down-inner ${PREFIX.icon} ${PREFIX.icon}-ios-arrow-down"></span>
</a>
</div>
<div class="${PREFIX.inputnb}-input-wrap">
<input autocomplete="off" spellcheck="false" class="${PREFIX.inputnb}-input">
</div>
`;
setHtml(node, template);
}
private _setOutSide(node: HTMLElement, controlsOutside: boolean): void {
if (!controlsOutside) return;
node.classList.add(`${PREFIX.inputnb}-controls-outside`);
const handlerWrap = node.querySelector(`.${PREFIX.inputnb}-handler-wrap`)!;
const template = `
<div class="${PREFIX.inputnb}-controls-outside-btn ${PREFIX.inputnb}-controls-outside-down">
<i class="${PREFIX.icon} ${PREFIX.icon}-ios-remove"></i>
</div>
<div class="${PREFIX.inputnb}-controls-outside-btn ${PREFIX.inputnb}-controls-outside-up">
<i class="${PREFIX.icon} ${PREFIX.icon}-ios-add"></i>
</div>
`;
handlerWrap.insertAdjacentHTML('afterend', template);
handlerWrap.remove();
}
private _setInput(
input: HTMLInputElement,
min: number,
max: number,
step: number,
name: string,
inputId: string,
placeholder: string
): void {
isNaN(min) || min === 0 ? (input.min = `${min}`) : '';
isNaN(max) || min === 0 ? (input.max = `${max}`) : '';
isNaN(step) && step !== 1 ? (input.step = `${step}`) : '';
name ? (input.name = name) : '';
inputId ? (input.id = inputId) : '';
placeholder ? (input.placeholder = placeholder) : '';
}
private _formatterVal(input: HTMLInputElement, formatter: string, val: number): void {
// `约定的 ${value}`替换为 `${val}`
const resVal = formatter.replace('value', 'val');
input.value = `${formatter ? eval(resVal) : val}`;
}
private _parserVal(parser: string, val: string): string {
if (parser) {
const _parser = eval(parser) as any[];
return val.replace(_parser[0], _parser[1]);
} else {
// 如果没有指定从 formatter 里转换回数字的方式,则使用默认正则方式
return val.replace(/[^\d.-]/g, '');
}
}
private _handleChange(
input: HTMLInputElement,
aUp: Element | null,
aDown: Element | null,
btnUp: Element | null,
btnDown: Element | null,
min: number,
max: number,
step: number,
precision: number,
readOnly: boolean,
parser: string,
formatter: string
): void {
if (readOnly) return;
const setValue = (val: number) => {
this._setValue(input, val, formatter, precision, min, max);
this._setHandler(aUp, aDown, btnUp, btnDown, val, min, max);
};
const changeStep = (type: 'up' | 'down'): false | undefined => {
// 如果指定了输入框展示值的格式,那么这里就要用 parser 的值转换为原来的值
const val = this._parserVal(parser, input.value);
const targetVal = Number(val);
if (type === 'up') {
if (addNum(targetVal, step) <= max) {
setValue(targetVal);
} else {
return false;
}
setValue(addNum(targetVal, step));
} else if (type === 'down') {
if (addNum(targetVal, step) >= min) {
setValue(targetVal);
} else {
return false;
}
setValue(addNum(targetVal, -step));
}
};
const handleKeyBoardChange = () => {
bind(input, 'keydown', (e: KeyboardEvent) => {
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return false;
if (e.key === 'ArrowUp') {
e.preventDefault();
changeStep('up');
}
if (e.key === 'ArrowDown') {
e.preventDefault();
changeStep('down');
}
});
};
const handleInputChange = () => {
bind(input, 'input', (e: InputEvent) => {
e.stopPropagation();
// 当输入框输入时只匹配数字、小数点和减号
const val = input.value.replace(/[^\d.-]/g, '');
setValue(Number(val));
});
};
const handleArrowChange = () => {
if (aUp && aDown) {
bind(aUp, 'click', () => changeStep('up'));
bind(aDown, 'click', () => changeStep('down'));
}
if (btnUp && btnDown) {
bind(btnUp, 'click', () => changeStep('up'));
bind(btnDown, 'click', () => changeStep('down'));
}
};
handleKeyBoardChange();
handleInputChange();
handleArrowChange();
}
private _setValue(
input: HTMLInputElement,
value: number,
formatter: string,
precision: number,
min: number,
max: number
): void {
let targetVal: any = !isNaN(precision) ? value.toFixed(precision) : value;
if ((targetVal && !isNaN(targetVal)) || targetVal === 0) {
if (targetVal > max && !isNaN(max)) {
targetVal = max;
} else if (targetVal < min && !isNaN(min)) {
targetVal = min;
}
// 如果指定了输入框展示值的格式则使用它,否则反之
this._formatterVal(input, formatter, targetVal);
}
}
private _setSize(node: Element, size: string): void {
if (!size) return;
node.classList.add(`${PREFIX.inputnb}-${size}`);
}
private _setReadonlyAndEditable(
input: HTMLInputElement,
readOnly: boolean,
editable: string
): void {
if (readOnly) input.readOnly = true;
if (readOnly || editable === 'false') input.style.pointerEvents = 'none';
}
private _setDisabled(node: HTMLElement, input: HTMLInputElement, disabled: boolean): void {
if (!disabled) {
node.classList.remove(`${PREFIX.inputnb}-disabled`);
input.disabled = false;
} else {
node.classList.add(`${PREFIX.inputnb}-disabled`);
input.disabled = true;
}
}
private _setHandler(
aUp: Element | null,
aDown: Element | null,
btnUp: Element | null,
btnDown: Element | null,
value: number,
min: number,
max: number
): void {
const isSetDisable = (elm1: Element, elm2: Element, outside: boolean) => {
const upDisabledCls = outside ? 'controls-outside-btn' : 'handler-up';
const downDisabledCls = outside ? 'controls-outside-btn' : 'handler-down';
if (Math.ceil(value) >= max) {
elm1.classList.add(`${PREFIX.inputnb}-${upDisabledCls}-disabled`);
} else {
elm1.classList.remove(`${PREFIX.inputnb}-${upDisabledCls}-disabled`);
}
if (Math.ceil(value) <= min) {
elm2.classList.add(`${PREFIX.inputnb}-${downDisabledCls}-disabled`);
} else {
elm2.classList.remove(`${PREFIX.inputnb}-${downDisabledCls}-disabled`);
}
};
if (aUp && aDown) isSetDisable(aUp, aDown, false);
if (btnUp && btnDown) isSetDisable(btnUp, btnDown, true);
}
private _attrs(node: HTMLElement) {
return {
min: getNumTypeAttr(node, 'min', -Infinity),
max: getNumTypeAttr(node, 'max', Infinity),
step: getNumTypeAttr(node, 'step', 1),
value: getNumTypeAttr(node, 'value', 0),
precision: getNumTypeAttr(node, 'precision'),
size: getStrTypeAttr(node, 'size', ''),
name: getStrTypeAttr(node, 'name', ''),
inputId: getStrTypeAttr(node, 'input-id', ''),
parser: getStrTypeAttr(node, 'parser', ''),
formatter: getStrTypeAttr(node, 'formatter', ''),
placeholder: getStrTypeAttr(node, 'placeholder', ''),
disabled: getBooleanTypeAttr(node, 'disabled'),
readOnly: getBooleanTypeAttr(node, 'readonly'),
editable: getStrTypeAttr(node, 'editable', 'true'),
controlsOutside: getBooleanTypeAttr(node, 'controls-outside')
};
}
}
export default InputNumber;