- fastVue.base.js: __onInput 粘贴分支原把显示值当模型值传入 setValue 导致 percent 字段 ×100; 改为按 __change 同款口径先解析为模型值再回显,键盘输入路径不变 - 新增 numberInput.paste.test.js 覆盖 percent:true/append:%/append:‱/普通数字 4 类字段的 Ctrl+V 粘贴 - fe-tests package.json 增加 jest-environment-jsdom、jquery 依赖
593 lines
21 KiB
JavaScript
593 lines
21 KiB
JavaScript
|
||
(function (window, $) {
|
||
|
||
function FastVue() {
|
||
|
||
}
|
||
|
||
FastVue.noop = function (a, b, c) { };
|
||
|
||
//解析数字,返回值:NAN或正常数字或''(输入参数为空)
|
||
FastVue.parseNumber = function (val,percent) {
|
||
|
||
switch (typeof val) {
|
||
case 'number': return val;
|
||
case 'string':
|
||
{
|
||
val = val.trim();
|
||
|
||
if (val === '') return '';
|
||
|
||
let isPercent = val.endsWith('%') || percent==true;
|
||
|
||
if (isPercent) {
|
||
val = val.replace(/%+$/, '');
|
||
if (val === '') return '';
|
||
}
|
||
if (val.endsWith('‱')) {
|
||
val = val.replace(/‱+$/, '');
|
||
if (val === '') return '';
|
||
}
|
||
let number = new Number(val);
|
||
|
||
return isPercent ? number / 100 : number;
|
||
}
|
||
default: return Number.NaN;
|
||
}
|
||
};
|
||
|
||
FastVue.extend = $.extend;
|
||
|
||
FastVue.setWidth = function (el, width) {
|
||
width && (el.style.width = typeof width === 'number' ? width + 'px' : width);
|
||
};
|
||
|
||
FastVue.getDomElement = function (el) {
|
||
if (!el) return null;
|
||
if (typeof el === 'string') {
|
||
return document.getElementById(el.startsWith('#') ? el.substring(1) : el);
|
||
}
|
||
el instanceof jQuery && (el = el[0]);
|
||
return el;
|
||
};
|
||
|
||
function Form(options) {
|
||
|
||
if (!(this instanceof Form)) {
|
||
throw 'FastVue.Form is a constructor and should be called with the `new` keyword';
|
||
}
|
||
|
||
this.data = {};
|
||
|
||
this.layout = {
|
||
display: 'inline',
|
||
title: {
|
||
width: 100
|
||
},
|
||
input: {
|
||
width: '165px'
|
||
}
|
||
};
|
||
|
||
this.submit = {
|
||
url: '',
|
||
before() { },
|
||
after() { }
|
||
};
|
||
|
||
this.fields = {};
|
||
|
||
this.view = {};
|
||
|
||
this.init(options);
|
||
}
|
||
|
||
FastVue.Form = Form;
|
||
|
||
window.FastVue = FastVue;
|
||
|
||
}(window, jQuery));
|
||
|
||
//autocomplete
|
||
(function (window, $) {
|
||
|
||
function __resetLookup(options, context) {
|
||
|
||
if (options.lookupFn) {
|
||
let lookupFn = options.lookupFn;
|
||
return options.lookup = function (query, callback) {
|
||
let queryLowerCase = (query || '').toLowerCase();
|
||
let suggestions = lookupFn.call(context) || [];
|
||
if (queryLowerCase) {
|
||
if (options.searchField) {
|
||
suggestions = suggestions.filter(data => {
|
||
return options.searchField.some(x => (data[x] || '').toLowerCase().indexOf(queryLowerCase) !== -1);
|
||
});
|
||
} else {
|
||
suggestions = suggestions.filter(data => {
|
||
return (data[options.valueField] || '').toLowerCase().indexOf(queryLowerCase) !== -1;
|
||
});
|
||
}
|
||
}
|
||
|
||
if (typeof suggestions[0] !== 'object') {
|
||
suggestions = suggestions.map(x => new Object({ value: x, data: x }));
|
||
} else {
|
||
suggestions = suggestions ? suggestions.map(x => {
|
||
return { value: x[options.nameField], data: x };
|
||
}) : [];
|
||
}
|
||
|
||
callback({ suggestions: suggestions });
|
||
};
|
||
}
|
||
|
||
if (!options.lookup) return;
|
||
|
||
if (typeof options.lookup === 'function') {
|
||
let lookupFn = options.lookup;
|
||
return options.lookup = function (query, callback) {
|
||
let queryLowerCase = (query || '').toLowerCase();
|
||
let suggestions = lookupFn.call(context, queryLowerCase) || [];
|
||
if (typeof suggestions[0] !== 'object') {
|
||
suggestions = suggestions.map(x => new Object({ value: x, data: x }));
|
||
} else {
|
||
suggestions = suggestions.map(x => {
|
||
return { value: x[options.nameField], data: x };
|
||
});
|
||
}
|
||
callback({ suggestions: suggestions });
|
||
};
|
||
}
|
||
|
||
if (Array.isArray(options.lookup)) {
|
||
|
||
let lookup = options.lookup;
|
||
|
||
if (typeof lookup[0] !== 'object') {
|
||
return options.lookup = lookup.map(x => new Object({ value: x, data: x }));
|
||
}
|
||
|
||
options.lookup = lookup.map(x => {
|
||
return { value: x[options.nameField], data: x };
|
||
});
|
||
|
||
if (options.searchField) {
|
||
options.lookupFilter = function (suggestion, originalQuery, queryLowerCase) {
|
||
return options.searchField.some(x => (suggestion.data[x] || '').toLowerCase().indexOf(queryLowerCase) !== -1);
|
||
};
|
||
} else {
|
||
options.lookupFilter = function (suggestion, originalQuery, queryLowerCase) {
|
||
return (suggestion.value || '').toLowerCase().indexOf(queryLowerCase) !== -1;
|
||
};
|
||
}
|
||
|
||
} else {
|
||
throw '[autocomplete.lookup]必须为数组或函数';
|
||
}
|
||
}
|
||
|
||
FastVue.autocomplete = function (el, options, context) {
|
||
|
||
!context && (context = this);
|
||
|
||
el = FastVue.getDomElement(el)
|
||
|
||
if (!el) throw '[FastVue.autocomplete]el is null or undefined:' + (options ? options.debugInfo : '');
|
||
|
||
let _el = el;//_el对应nameField
|
||
|
||
options = Object.assign({ minChars: 0, autoSelectFirst: true }, options);
|
||
|
||
!options.valueField && (options.valueField = options.nameField || 'value');
|
||
!options.nameField && (options.nameField = options.valueField);
|
||
|
||
if (options.nameField !== options.valueField) {
|
||
_el = el.cloneNode(true);
|
||
_el.setAttribute('name', '');
|
||
_el.setAttribute('value', '');
|
||
_el.id && _el.setAttribute('id', _el.id + '_auto');
|
||
el.setAttribute('type', 'hidden');
|
||
if (!el.parentNode) {
|
||
document.createElement("div").append(el);
|
||
}
|
||
el.parentNode.insertBefore(_el, el);
|
||
el.dataset.autocompleteClone = _el;
|
||
}
|
||
|
||
__resetLookup(options, context);
|
||
|
||
const onSelectFn = options.onSelect;
|
||
options.onSelect = function (suggestion) {
|
||
_el.dataset.select = '1';
|
||
if (options.nameField !== options.valueField) {
|
||
el.value = suggestion.data[options.valueField];
|
||
}
|
||
onSelectFn && onSelectFn(suggestion.data);
|
||
};
|
||
|
||
$(_el).autocomplete('dispose');
|
||
|
||
$(_el).on('focus', function () {
|
||
this.setAttribute("placeholder", this.value);
|
||
this.value = '';
|
||
this.dataset.select = '';
|
||
}).on('blur', function () {
|
||
!this.dataset.select && (this.value = this.getAttribute("placeholder"));
|
||
this.setAttribute("placeholder", '');
|
||
}).on('keydown', function (event) {
|
||
if ((event.which || event.keyCode) === 13) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
$(this).blur();
|
||
}
|
||
}).autocomplete(options);
|
||
|
||
return {
|
||
setData(data) {
|
||
if (options.nameField === options.valueField) {
|
||
_el.value = (typeof data === 'object' ? data[options.nameField] : data) || '';
|
||
} else if (data) {
|
||
el.value = data[options.valueField] || '';
|
||
_el.value = data[options.nameField] || '';
|
||
} else {
|
||
_el.value = el.value = '';
|
||
}
|
||
},
|
||
dispose() {
|
||
$(_el).autocomplete('dispose');
|
||
$(_el).remove();
|
||
},
|
||
toggleDisabled(disabled) {
|
||
_el.disabled = typeof disabled === 'undefined' ? !_el.disabled : !!disabled;
|
||
},
|
||
clearCache() {
|
||
$(_el).autocomplete('clearCache');
|
||
},
|
||
setOptions(newOptions) {
|
||
$(_el).autocomplete('setOptions', Object.assign({}, options, newOptions));
|
||
}
|
||
};
|
||
};
|
||
|
||
//静态dispose方法
|
||
FastVue.autocomplete.dispose = function (el) {
|
||
(el instanceof jQuery) && (el = el.get(0));
|
||
if (!el) return;
|
||
$(el).autocomplete('dispose');
|
||
el.dataset && $(el.dataset.autocompleteClone).autocomplete('dispose');
|
||
};
|
||
|
||
}(FastVue, jQuery));
|
||
|
||
//numberInput
|
||
(function (window, $) {
|
||
|
||
FastVue.numberInput = function (el, options) {
|
||
|
||
let _el = typeof el === 'string' ? document.getElementById(el.replace(/^#/, '')) : el instanceof jQuery ? el[0] : el;
|
||
|
||
if (!_el) {
|
||
throw "[FastVue.numberInput]not found el:" + el;
|
||
}
|
||
|
||
let _ctrlV = false, _chnInput = -1;
|
||
|
||
let _options = { append: '', precision: 2, negative: false, grouping: false };
|
||
|
||
function getValue() {
|
||
let val = _el.value;
|
||
if (val && val !== _options.append) {
|
||
_options.append && _options.append !== '%' && _options.append !== '‱' && (val = val.replace(new RegExp(_options.append + "$"), ''));
|
||
return FastVue.parseNumber(val, _options.percent);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function setOptions(options) {
|
||
options = Object.assign(_options, options);
|
||
!options.append && (options.append = '');
|
||
let precision = parseInt(options.precision) || 0;
|
||
options.precision = precision < 0 ? 0 : precision;
|
||
options.negative = !!options.negative;
|
||
_options = options;
|
||
setValue(getValue());
|
||
}
|
||
|
||
function setValue(value) {
|
||
if (value || value === 0) {
|
||
let oval = parseFloat(value) || 0;
|
||
if (_options.append === '%' || _options.percent == true) {
|
||
oval *= 100;
|
||
}
|
||
else if (_options.append ==="‱") {
|
||
oval *= 10000;
|
||
}
|
||
if (_options.precision > 0) {
|
||
let str = main.formatNumber(oval, _options.precision, { grouping: _options.grouping });
|
||
if (typeof value === 'number' && str.endsWith("0") && str.includes('.')) {
|
||
str = str.replace(/\.?0+$/, '');
|
||
}
|
||
_el.value = str + _options.append;
|
||
} else {
|
||
let str = main.formatNumber(Math.floor(oval), 0, { grouping: _options.grouping });
|
||
_el.value = str + _options.append;
|
||
}
|
||
} else {
|
||
_el.value = '';
|
||
}
|
||
}
|
||
|
||
function checkDotInput() {
|
||
if (this.selectionStart >= this.value.length) {
|
||
return _options.append && this.value.endsWith(_options.append) ? false : this.value.indexOf('.') < 0;
|
||
}
|
||
if (this.value.length - _options.append.length - this.selectionEnd > _options.precision) {
|
||
return false;
|
||
}
|
||
return this.value.substring(0, this.selectionStart).indexOf('.') < 0 && this.value.substring(this.selectionEnd, this.value.length).indexOf('.') < 0;
|
||
}
|
||
|
||
function __keyHandle(event) {
|
||
if (!event) return false;
|
||
|
||
_chnInput = -1;
|
||
|
||
let charCode = event.which || event.keyCode;
|
||
|
||
switch (charCode) {
|
||
case 8: //break space
|
||
return !_options.append || !this.value.endsWith(_options.append) || this.selectionStart < this.value.length - _options.append.length + 1;
|
||
case 9: //tab
|
||
case 35: //end
|
||
case 36: //home
|
||
case 37: //left arrow
|
||
case 39: //right arrow
|
||
case 42: //print
|
||
case 116: //F5
|
||
return true;
|
||
case 46: //del
|
||
return !_options.append || !this.value.endsWith(_options.append) || this.selectionStart < this.value.length - _options.append.length;
|
||
case 229:
|
||
return _chnInput = this.selectionStart;
|
||
}
|
||
|
||
if (event.shiftKey) return false;
|
||
|
||
switch (charCode) {
|
||
case 109: //negative sign(-)
|
||
return _options.negative && this.selectionStart < 1 && this.value.charAt(0) !== '-';
|
||
case 189: //negative sign(-_)
|
||
return _options.negative && !event.shiftKey && this.selectionStart < 1 && this.value.charAt(0) !== '-';
|
||
case 110: //dot(.)
|
||
return _options.precision > 0 && checkDotInput.call(this);
|
||
case 190: //dot(.>)
|
||
return _options.precision > 0 && !event.shiftKey && checkDotInput.call(this);
|
||
case 13: //enter
|
||
case 108: //enter
|
||
$(this).blur();
|
||
return true;
|
||
case 67: //ctrl+c
|
||
return event.ctrlKey;
|
||
case 86: //ctrl+v
|
||
return event.ctrlKey && (_ctrlV = true);
|
||
default:
|
||
if (charCode < 48 || charCode > 57 && (charCode < 96 || charCode > 105)) {
|
||
return false;
|
||
}
|
||
if (this.selectionStart === 0) {
|
||
return !this.value || this.value.charAt(this.selectionEnd) !== '-';
|
||
}
|
||
if (_options.precision < 1) {
|
||
return true;
|
||
}
|
||
if (this.selectionStart >= this.value.length && _options.append && this.value.endsWith(_options.append)) {
|
||
return false;
|
||
}
|
||
if (this.selectionEnd > this.selectionStart) {
|
||
return /\d/.test(this.value.substring(this.selectionStart, this.selectionEnd));
|
||
} else {
|
||
let dotIndex = this.value.lastIndexOf('.', this.selectionStart - 1);
|
||
return dotIndex < 0 || !/\d/.test(this.value[dotIndex + _options.precision]);
|
||
}
|
||
}
|
||
}
|
||
|
||
//处理中文字符输入
|
||
function __onChineseInput(chnPos) {
|
||
if (this.value.length < chnPos) {
|
||
chnPos = this.value.length - 1;
|
||
}
|
||
let ch = this.value.charCodeAt(chnPos);
|
||
if (ch > 47 && ch < 58) {
|
||
chnPos += 1;
|
||
}
|
||
else if (ch === 46 || ch === 12290) {
|
||
if (_options.precision > 0 && !/\d/.test(this.value[this.selectionStart + _options.precision])
|
||
&& this.value.lastIndexOf('.', chnPos - 1) < 0 && this.value.indexOf('.', this.selectionStart) < 0) {
|
||
if (ch === 12290) {
|
||
this.value = this.value.substring(0, chnPos) + '.' + this.value.substring(this.selectionStart);
|
||
this.setSelectionRange(chnPos + 1, chnPos + 1);
|
||
return;
|
||
}
|
||
chnPos += 1;
|
||
}
|
||
}
|
||
else if (chnPos === 0 && ch === 45 && _options.negative && this.value[this.selectionStart] !== '-') { //负号
|
||
return;
|
||
} else {
|
||
for (; chnPos >= 0; chnPos--) {
|
||
ch = this.value.charCodeAt(chnPos);
|
||
if (ch === 46 || ch > 47 && ch < 58 || chnPos === 0 && ch === 45 && _options.negative) {
|
||
chnPos += 1;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
this.value = this.value.substring(0, chnPos) + this.value.substring(this.selectionStart);
|
||
this.setSelectionRange(chnPos, chnPos);
|
||
}
|
||
|
||
//处理字符输入
|
||
function __onInput() {
|
||
if (_ctrlV) {
|
||
_ctrlV = false;
|
||
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
|
||
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
|
||
let f = '';
|
||
if (this.value && this.value !== _options.append) {
|
||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||
else if (_options.append === '‱') f /= 10000;
|
||
}
|
||
return setValue(f);
|
||
}
|
||
if (_chnInput >= 0) {
|
||
__onChineseInput.call(this, _chnInput);
|
||
}
|
||
if (!_options.append || !this.value) return;
|
||
let appended = true;
|
||
if (this.value !== _options.append) {
|
||
appended = this.value.endsWith(_options.append);
|
||
}
|
||
if (!appended) {
|
||
let caretPos = this.selectionStart;
|
||
this.value += _options.append;
|
||
this.setSelectionRange(caretPos, caretPos);
|
||
} else if (_options.negative && this.selectionStart === 2 && this.value[1] === '-') {
|
||
this.value = this.value.substring(1);
|
||
this.setSelectionRange(1, 1);
|
||
}
|
||
}
|
||
|
||
//输入完成处理
|
||
function __change() {
|
||
let f = '';
|
||
if (this.value && this.value !== _options.append) {
|
||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||
else if (_options.append === '‱') f /= 10000;
|
||
|
||
}
|
||
_options.onchange && _options.onchange(f, this.value);
|
||
}
|
||
|
||
//功能禁用
|
||
function __disable() {
|
||
return false;
|
||
}
|
||
|
||
setOptions(options);
|
||
|
||
$(_el).on('keydown', __keyHandle).on('dragenter', __disable).on('input', __onInput).on('change', __change);
|
||
|
||
return {
|
||
isEmpty() {
|
||
return !_el.value || _el.value === _options.append;
|
||
},
|
||
getValue: getValue,
|
||
setValue: setValue,
|
||
setOptions: setOptions,
|
||
getOptions() {
|
||
return Object.assign({}, _options);
|
||
},
|
||
dispose() {
|
||
$(_el).off();
|
||
}
|
||
};
|
||
};
|
||
|
||
//静态dispose方法
|
||
FastVue.numberInput.dispose = function (el) {
|
||
(el instanceof jQuery ? el : $(el)).off();
|
||
};
|
||
|
||
}(FastVue, jQuery));
|
||
|
||
//FastVue.vueNumberInput
|
||
//数字输入组件(借助FastVue.numberInput)
|
||
(function (global) {
|
||
|
||
global.vueNumberInput = function () {
|
||
return {
|
||
props: {
|
||
value: {
|
||
type: Number,
|
||
default: ''
|
||
},
|
||
format: {
|
||
type: Object,
|
||
default: {
|
||
append: '',
|
||
precision: 2,
|
||
negative: false,
|
||
grouping: false,
|
||
percent: false
|
||
}
|
||
},
|
||
disabled: {
|
||
type: Boolean
|
||
},
|
||
ret_type: {
|
||
type: Number,
|
||
default: 0
|
||
}
|
||
},
|
||
data() {
|
||
return { inputctrl: null,init:true};
|
||
},
|
||
mounted() {
|
||
let self = this;
|
||
let options = FastVue.extend({ onchange: this.onchange }, this.format);
|
||
this.inputctrl = FastVue.numberInput(this.$el, options);
|
||
this.inputctrl.setValue(this.value)
|
||
},
|
||
methods: {
|
||
onchange(value, text) {
|
||
this.init = false;
|
||
this.$emit('input', this.ret_type === 1 ? text : value);
|
||
}
|
||
},
|
||
watch: {
|
||
value(val, old) {
|
||
let val1 = '';
|
||
switch (typeof val) {
|
||
case 'number':
|
||
val1 = val || 0; break;
|
||
case 'string':
|
||
if (val) {
|
||
val1 = parseFloat(val) || 0;
|
||
if (val1 && (val.trimEnd().endsWith('%') || (this.format.percent && !this.init))) {
|
||
val1 /= 100;
|
||
} else if (val1 && (val.trimEnd().endsWith('‱'))){
|
||
val1 /= 10000;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
if (!this.inputctrl.isEmpty()) {
|
||
let val2 = this.inputctrl.getValue();
|
||
if (Math.abs(val1 - val2) < 1e-10) return;
|
||
}
|
||
this.$nextTick(function () {
|
||
this.inputctrl.setValue(val1);
|
||
});
|
||
},
|
||
format: {
|
||
deep: true,
|
||
handler(val) {
|
||
this.inputctrl.setOptions(val);
|
||
this.$nextTick(function () {
|
||
this.inputctrl.setValue(this.value);
|
||
});
|
||
}
|
||
}
|
||
},
|
||
destroyed() {
|
||
FastVue.numberInput.dispose(this.$el);
|
||
},
|
||
template: '<input type="text" v-bind:disabled="disabled">'
|
||
};
|
||
};
|
||
|
||
}(window.FastVue)); |