You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1042 lines
39 KiB
1042 lines
39 KiB
/**
|
|
* Bootstrap Search Suggest
|
|
* @desc 这是一个基于 bootstrap 按钮式下拉菜单组件的搜索建议插件,必须使用于按钮式下拉菜单组件上。
|
|
* @author renxia <lzwy0820#qq.com>
|
|
* @github https://github.com/lzwme/bootstrap-suggest-plugin.git
|
|
* @since 2014-10-09
|
|
*===============================================================================
|
|
* (c) Copyright 2014-2019 http://lzw.me All Rights Reserved.
|
|
********************************************************************************/
|
|
(function (factory) {
|
|
if (typeof define === "function" && define.amd) {
|
|
define(['jquery'], factory);
|
|
} else if (typeof exports === 'object' && typeof module === 'object') {
|
|
factory(require('jquery'));
|
|
} else if (window.jQuery) {
|
|
factory(window.jQuery);
|
|
} else {
|
|
throw new Error('Not found jQuery.');
|
|
}
|
|
})(function($) {
|
|
var VERSION = 'VERSION_PLACEHOLDER';
|
|
var $window = $(window);
|
|
var isIe = 'ActiveXObject' in window; // 用于对 IE 的兼容判断
|
|
var inputLock; // 用于中文输入法输入时锁定搜索
|
|
|
|
// ie 下和 chrome 51 以上浏览器版本,出现滚动条时不计算 padding
|
|
var chromeVer = navigator.userAgent.match(/Chrome\/(\d+)/);
|
|
if (chromeVer) {
|
|
chromeVer = +chromeVer[1];
|
|
}
|
|
var notNeedCalcPadding = isIe || chromeVer > 51;
|
|
|
|
// 一些常量
|
|
var BSSUGGEST = 'bsSuggest';
|
|
var onDataRequestSuccess = 'onDataRequestSuccess';
|
|
var DISABLED = 'disabled';
|
|
var TRUE = true;
|
|
var FALSE = false;
|
|
|
|
function isUndefined(val) {
|
|
return val === void(0);
|
|
}
|
|
|
|
/**
|
|
* 错误处理
|
|
*/
|
|
function handleError(e1, e2) {
|
|
if (!window.console || !window.console.trace) {
|
|
return;
|
|
}
|
|
console.trace(e1);
|
|
if (e2) {
|
|
console.trace(e2);
|
|
}
|
|
}
|
|
/**
|
|
* 获取当前 tr 列的关键字数据
|
|
*/
|
|
function getPointKeyword($list) {
|
|
return $list.data();
|
|
}
|
|
/**
|
|
* 设置或获取输入框的 alt 值
|
|
*/
|
|
function setOrGetAlt($input, val) {
|
|
return isUndefined(val) ? $input.attr('alt') : $input.attr('alt', val);
|
|
}
|
|
/**
|
|
* 设置或获取输入框的 data-id 值
|
|
*/
|
|
function setOrGetDataId($input, val) {
|
|
return val !== (void 0) ? $input.attr('data-id', val) : $input.attr('data-id');
|
|
}
|
|
/**
|
|
* 设置选中的值
|
|
*/
|
|
function setValue($input, keywords, options) {
|
|
if (!keywords || !keywords.key) {
|
|
return;
|
|
}
|
|
|
|
var separator = options.separator || ',',
|
|
inputValList,
|
|
inputIdList,
|
|
dataId = setOrGetDataId($input);
|
|
|
|
if (options && options.multiWord) {
|
|
inputValList = $input.val().split(separator);
|
|
inputValList[inputValList.length - 1] = keywords.key;
|
|
|
|
//多关键字检索支持设置id --- 存在 bug,不建议使用
|
|
if (!dataId) {
|
|
inputIdList = [keywords.id];
|
|
} else {
|
|
inputIdList = dataId.split(separator);
|
|
inputIdList.push(keywords.id);
|
|
}
|
|
|
|
setOrGetDataId($input, inputIdList.join(separator))
|
|
.val(inputValList.join(separator))
|
|
.focus();
|
|
} else {
|
|
setOrGetDataId($input, keywords.id || '').val(keywords.key).focus();
|
|
}
|
|
|
|
$input.data('pre-val', $input.val())
|
|
.trigger('onSetSelectValue', [keywords, (options.data.value || options._lastData.value)[keywords.index]]);
|
|
}
|
|
/**
|
|
* 调整选择菜单位置
|
|
* @param {Object} $input
|
|
* @param {Object} $dropdownMenu
|
|
* @param {Object} options
|
|
*/
|
|
function adjustDropMenuPos($input, $dropdownMenu, options) {
|
|
if (!$dropdownMenu.is(':visible')) {
|
|
return;
|
|
}
|
|
|
|
var $parent = $input.parent();
|
|
var parentHeight = $parent.height();
|
|
var parentWidth = $parent.width();
|
|
|
|
if (options.autoDropup) {
|
|
setTimeout(function() {
|
|
var offsetTop = $input.offset().top;
|
|
var winScrollTop = $window.scrollTop();
|
|
var menuHeight = $dropdownMenu.height();
|
|
|
|
if ( // 自动判断菜单向上展开
|
|
($window.height() + winScrollTop - offsetTop) < menuHeight && // 假如向下会撑长页面
|
|
offsetTop > (menuHeight + winScrollTop) // 而且向上不会撑到顶部
|
|
) {
|
|
$parent.addClass('dropup');
|
|
} else {
|
|
$parent.removeClass('dropup');
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
// 列表对齐方式
|
|
var dmcss = {};
|
|
if (options.listAlign === 'left') {
|
|
dmcss = {
|
|
'left': $input.siblings('div').width() - parentWidth,
|
|
'right': 'auto'
|
|
};
|
|
} else if (options.listAlign === 'right') {
|
|
dmcss = {
|
|
'left': 'auto',
|
|
'right': 0
|
|
};
|
|
}
|
|
|
|
// ie 下,不显示按钮时的 top/bottom
|
|
if (isIe && !options.showBtn) {
|
|
if (!$parent.hasClass('dropup')) {
|
|
dmcss.top = parentHeight;
|
|
dmcss.bottom = 'auto';
|
|
} else {
|
|
dmcss.top = 'auto';
|
|
dmcss.bottom = parentHeight;
|
|
}
|
|
}
|
|
|
|
// 是否自动最小宽度
|
|
if (!options.autoMinWidth) {
|
|
dmcss.minWidth = parentWidth;
|
|
}
|
|
/* else {
|
|
dmcss['width'] = 'auto';
|
|
}*/
|
|
|
|
$dropdownMenu.css(dmcss);
|
|
|
|
return $input;
|
|
}
|
|
/**
|
|
* 设置输入框背景色
|
|
* 当设置了 indexId,而输入框的 data-id 为空时,输入框加载警告色
|
|
*/
|
|
function setBackground($input, options) {
|
|
var inputbg, bg, warnbg;
|
|
if ((options.indexId === -1 && !options.idField) || options.multiWord) {
|
|
return $input;
|
|
}
|
|
|
|
bg = options.inputBgColor;
|
|
warnbg = options.inputWarnColor;
|
|
|
|
var curVal = $input.val();
|
|
var preVal = $input.data('pre-val');
|
|
|
|
if (setOrGetDataId($input) || !curVal) {
|
|
$input.css('background', bg || '');
|
|
|
|
if (!curVal && preVal) {
|
|
$input.trigger('onUnsetSelectValue').data('pre-val', '');
|
|
}
|
|
|
|
return $input;
|
|
}
|
|
|
|
inputbg = $input.css('backgroundColor').replace(/ /g, '').split(',', 3).join(',');
|
|
// 自由输入的内容,设置背景色
|
|
if (!~warnbg.indexOf(inputbg)) {
|
|
$input.trigger('onUnsetSelectValue') // 触发取消data-id事件
|
|
.data('pre-val', '')
|
|
.css('background', warnbg);
|
|
}
|
|
|
|
return $input;
|
|
}
|
|
/**
|
|
* 调整滑动条
|
|
*/
|
|
function adjustScroll($input, $dropdownMenu, options) {
|
|
// 控制滑动条
|
|
var $hover = $input.parent().find('tbody tr.' + options.listHoverCSS),
|
|
pos, maxHeight;
|
|
|
|
if ($hover.length) {
|
|
pos = ($hover.index() + 3) * $hover.height();
|
|
maxHeight = +$dropdownMenu.css('maxHeight').replace('px', '');
|
|
|
|
if (pos > maxHeight || $dropdownMenu.scrollTop() > maxHeight) {
|
|
pos = pos - maxHeight;
|
|
} else {
|
|
pos = 0;
|
|
}
|
|
|
|
$dropdownMenu.scrollTop(pos);
|
|
}
|
|
}
|
|
/**
|
|
* 解除所有列表 hover 样式
|
|
*/
|
|
function unHoverAll($dropdownMenu, options) {
|
|
$dropdownMenu.find('tr.' + options.listHoverCSS).removeClass(options.listHoverCSS);
|
|
}
|
|
/**
|
|
* 验证 $input 对象是否符合条件
|
|
* 1. 必须为 bootstrap 下拉式菜单
|
|
* 2. 必须未初始化过
|
|
*/
|
|
function checkInput($input, $dropdownMenu, options) {
|
|
if (
|
|
!$dropdownMenu.length || // 过滤非 bootstrap 下拉式菜单对象
|
|
$input.data(BSSUGGEST) // 是否已经初始化的检测
|
|
) {
|
|
return FALSE;
|
|
}
|
|
|
|
$input.data(BSSUGGEST, {
|
|
options: options
|
|
});
|
|
|
|
return TRUE;
|
|
}
|
|
/**
|
|
* 数据格式检测
|
|
* 检测 ajax 返回成功数据或 data 参数数据是否有效
|
|
* data 格式:{"value": [{}, {}...]}
|
|
*/
|
|
function checkData(data) {
|
|
var isEmpty = TRUE, o;
|
|
|
|
for (o in data) {
|
|
if (o === 'value') {
|
|
isEmpty = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
if (isEmpty) {
|
|
handleError('返回数据格式错误!');
|
|
return FALSE;
|
|
}
|
|
if (!data.value.length) {
|
|
// handleError('返回数据为空!');
|
|
return FALSE;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
/**
|
|
* 判断字段名是否在 options.effectiveFields 配置项中
|
|
* @param {String} field 要判断的字段名
|
|
* @param {Object} options
|
|
* @return {Boolean} effectiveFields 为空时始终返回 true
|
|
*/
|
|
function inEffectiveFields(field, options) {
|
|
var effectiveFields = options.effectiveFields;
|
|
|
|
return !(field === '__index' ||
|
|
effectiveFields.length &&
|
|
!~$.inArray(field, effectiveFields));
|
|
}
|
|
/**
|
|
* 判断字段名是否在 options.searchFields 搜索字段配置中
|
|
*/
|
|
function inSearchFields(field, options) {
|
|
return ~$.inArray(field, options.searchFields);
|
|
}
|
|
/**
|
|
* 通过下拉菜单显示提示文案
|
|
*/
|
|
function showTip(tip, $input, $dropdownMenu, options) {
|
|
$dropdownMenu.html('<div style="padding:10px 5px 5px">' + tip + '</div>').show();
|
|
adjustDropMenuPos($input, $dropdownMenu, options);
|
|
}
|
|
/**
|
|
* 显示下拉列表
|
|
*/
|
|
function showDropMenu($input, options) {
|
|
var $dropdownMenu = $input.parent().find('ul:eq(0)');
|
|
if (!$dropdownMenu.is(':visible')) {
|
|
// $dropdownMenu.css('display', 'block');
|
|
$dropdownMenu.show();
|
|
$input.trigger('onShowDropdown', [options ? options.data.value : []]);
|
|
}
|
|
}
|
|
/**
|
|
* 隐藏下拉列表
|
|
*/
|
|
function hideDropMenu($input, options) {
|
|
var $dropdownMenu = $input.parent().find('ul:eq(0)');
|
|
if ($dropdownMenu.is(':visible')) {
|
|
// $dropdownMenu.css('display', '');
|
|
$dropdownMenu.hide();
|
|
$input.trigger('onHideDropdown', [options ? options.data.value : []]);
|
|
}
|
|
}
|
|
/**
|
|
* 下拉列表刷新
|
|
* 作为 fnGetData 的 callback 函数调用
|
|
*/
|
|
function refreshDropMenu($input, data, options) {
|
|
var $dropdownMenu = $input.parent().find('ul:eq(0)'),
|
|
len, i, field, index = 0,
|
|
tds,
|
|
html = ['<table class="table table-condensed table-sm" style="margin:0">'],
|
|
idValue, keyValue; // 作为输入框 data-id 和内容的字段值
|
|
var dataList = data.value;
|
|
|
|
if (!data || !(len = dataList.length)) {
|
|
if (options.emptyTip) {
|
|
showTip(options.emptyTip, $input, $dropdownMenu, options);
|
|
} else {
|
|
$dropdownMenu.empty();
|
|
hideDropMenu($input, options);
|
|
}
|
|
return $input;
|
|
}
|
|
|
|
// 相同数据,不用继续渲染了
|
|
if (
|
|
options._lastData &&
|
|
JSON.stringify(options._lastData) === JSON.stringify(data) &&
|
|
$dropdownMenu.find('tr').length === len
|
|
) {
|
|
showDropMenu($input, options);
|
|
return adjustDropMenuPos($input, $dropdownMenu, options);
|
|
}
|
|
options._lastData = data;
|
|
|
|
// 生成表头
|
|
if (options.showHeader) {
|
|
html.push('<thead><tr>');
|
|
for (field in dataList[0]) {
|
|
if (!inEffectiveFields(field, options)) {
|
|
continue;
|
|
}
|
|
|
|
html.push('<th>', (options.effectiveFieldsAlias[field] || field),
|
|
index === 0 ? ('(' + len + ')') : '' , // 表头第一列记录总数
|
|
'</th>');
|
|
|
|
index++;
|
|
}
|
|
html.push('</tr></thead>');
|
|
}
|
|
html.push('<tbody>');
|
|
|
|
// console.log(data, len);
|
|
// 按列加数据
|
|
var dataI;
|
|
for (i = 0; i < len; i++) {
|
|
index = 0;
|
|
tds = [];
|
|
dataI = dataList[i];
|
|
idValue = dataI[options.idField];
|
|
keyValue = dataI[options.keyField];
|
|
|
|
for (field in dataI) {
|
|
// 标记作为 value 和 作为 id 的值
|
|
if (isUndefined(keyValue) && options.indexKey === index) {
|
|
keyValue = dataI[field];
|
|
}
|
|
if (isUndefined(idValue) && options.indexId === index) {
|
|
idValue = dataI[field];
|
|
}
|
|
|
|
index++;
|
|
|
|
// 列表中只显示有效的字段
|
|
if (inEffectiveFields(field, options)) {
|
|
tds.push('<td data-name="', field, '">', dataI[field], '</td>');
|
|
}
|
|
}
|
|
|
|
html.push('<tr data-index="', (dataI.__index || i),
|
|
'" data-id="', idValue,
|
|
'" data-key="', keyValue, '">',
|
|
tds.join(''), '</tr>');
|
|
}
|
|
html.push('</tbody></table>');
|
|
|
|
$dropdownMenu.html(html.join(''));
|
|
showDropMenu($input, options);
|
|
//.show();
|
|
|
|
// scrollbar 存在时,延时到动画结束时调整 padding
|
|
setTimeout(function() {
|
|
if (notNeedCalcPadding) {
|
|
return;
|
|
}
|
|
|
|
var $table = $dropdownMenu.find('table:eq(0)'),
|
|
pdr = 0,
|
|
mgb = 0;
|
|
|
|
if (
|
|
$dropdownMenu.height() < $table.height() &&
|
|
+$dropdownMenu.css('minWidth').replace('px', '') < $dropdownMenu.width()
|
|
) {
|
|
pdr = 18;
|
|
mgb = 20;
|
|
}
|
|
|
|
$dropdownMenu.css('paddingRight', pdr);
|
|
$table.css('marginBottom', mgb);
|
|
}, 301);
|
|
|
|
adjustDropMenuPos($input, $dropdownMenu, options);
|
|
|
|
return $input;
|
|
}
|
|
/**
|
|
* ajax 获取数据
|
|
* @param {Object} options
|
|
* @return {Object} $.Deferred
|
|
*/
|
|
function ajax(options, keyword) {
|
|
keyword = keyword || '';
|
|
|
|
var preAjax = options._preAjax;
|
|
|
|
if (preAjax && preAjax.abort && preAjax.readyState !== 4) {
|
|
// console.log('abort pre ajax');
|
|
preAjax.abort();
|
|
}
|
|
|
|
var ajaxParam = {
|
|
type: 'GET',
|
|
dataType: options.jsonp ? 'jsonp' : 'json',
|
|
timeout: 5000,
|
|
};
|
|
|
|
// jsonp
|
|
if (options.jsonp) {
|
|
ajaxParam.jsonp = options.jsonp;
|
|
}
|
|
|
|
// 自定义 ajax 请求参数生成方法
|
|
var adjustAjaxParam,
|
|
fnAdjustAjaxParam = options.fnAdjustAjaxParam;
|
|
|
|
if ($.isFunction(fnAdjustAjaxParam)) {
|
|
adjustAjaxParam = fnAdjustAjaxParam(keyword, options);
|
|
|
|
// options.fnAdjustAjaxParam 返回false,则终止 ajax 请求
|
|
if (FALSE === adjustAjaxParam) {
|
|
return;
|
|
}
|
|
|
|
$.extend(ajaxParam, adjustAjaxParam);
|
|
}
|
|
|
|
// url 调整
|
|
ajaxParam.url = function() {
|
|
if (!keyword || ajaxParam.data) {
|
|
return ajaxParam.url || options.url;
|
|
}
|
|
|
|
var type = '?';
|
|
if (/=$/.test(options.url)) {
|
|
type = '';
|
|
} else if (/\?/.test(options.url)) {
|
|
type = '&';
|
|
}
|
|
|
|
return options.url + type + encodeURIComponent(keyword);
|
|
}();
|
|
|
|
return options._preAjax = $.ajax(ajaxParam).done(function(result) {
|
|
options.data = options.fnProcessData(result);
|
|
}).fail(function(err) {
|
|
if (options.fnAjaxFail) {
|
|
options.fnAjaxFail(err, options);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* 检测 keyword 与 value 是否存在互相包含
|
|
* @param {String} keyword 用户输入的关键字
|
|
* @param {String} key 匹配字段的 key
|
|
* @param {String} value key 字段对应的值
|
|
* @param {Object} options
|
|
* @return {Boolean} 包含/不包含
|
|
*/
|
|
function isInWord(keyword, key, value, options) {
|
|
value = $.trim(value);
|
|
|
|
if (options.ignorecase) {
|
|
keyword = keyword.toLocaleLowerCase();
|
|
value = value.toLocaleLowerCase();
|
|
}
|
|
|
|
return value &&
|
|
(inEffectiveFields(key, options) || inSearchFields(key, options)) && // 必须在有效的搜索字段中
|
|
(
|
|
~value.indexOf(keyword) || // 匹配值包含关键字
|
|
options.twoWayMatch && ~keyword.indexOf(value) // 关键字包含匹配值
|
|
);
|
|
}
|
|
/**
|
|
* 通过 ajax 或 json 参数获取数据
|
|
*/
|
|
function getData(keyword, $input, callback, options) {
|
|
var data, validData, filterData = {
|
|
value: []
|
|
},
|
|
i, key, len,
|
|
fnPreprocessKeyword = options.fnPreprocessKeyword;
|
|
|
|
keyword = keyword || '';
|
|
// 获取数据前对关键字预处理方法
|
|
if ($.isFunction(fnPreprocessKeyword)) {
|
|
keyword = fnPreprocessKeyword(keyword, options);
|
|
}
|
|
|
|
// 给了url参数,则从服务器 ajax 请求
|
|
// console.log(options.url + keyword);
|
|
if (options.url) {
|
|
var timer;
|
|
if (options.searchingTip) {
|
|
timer = setTimeout(function() {
|
|
showTip(options.searchingTip, $input, $input.parent().find('ul'), options);
|
|
}, 600);
|
|
}
|
|
|
|
ajax(options, keyword).done(function(result) {
|
|
callback($input, options.data, options); // 为 refreshDropMenu
|
|
$input.trigger(onDataRequestSuccess, result);
|
|
if (options.getDataMethod === 'firstByUrl') {
|
|
options.url = null;
|
|
}
|
|
}).always(function() {
|
|
timer && clearTimeout(timer);
|
|
});
|
|
} else {
|
|
// 没有给出 url 参数,则从 data 参数获取
|
|
data = options.data;
|
|
validData = checkData(data);
|
|
// 本地的 data 数据,则在本地过滤
|
|
if (validData) {
|
|
if (keyword) {
|
|
// 输入不为空时则进行匹配
|
|
len = data.value.length;
|
|
for (i = 0; i < len; i++) {
|
|
for (key in data.value[i]) {
|
|
if (
|
|
data.value[i][key] &&
|
|
isInWord(keyword, key, data.value[i][key] + '', options)
|
|
) {
|
|
filterData.value.push(data.value[i]);
|
|
filterData.value[filterData.value.length - 1].__index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
filterData = data;
|
|
}
|
|
}
|
|
|
|
callback($input, filterData, options);
|
|
} // else
|
|
}
|
|
/**
|
|
* 数据处理
|
|
* url 获取数据时,对数据的处理,作为 fnGetData 之后的回调处理
|
|
*/
|
|
function processData(data) {
|
|
return checkData(data);
|
|
}
|
|
/**
|
|
* 取得 clearable 清除按钮
|
|
*/
|
|
function getIClear($input, options) {
|
|
var $iClear = $input.prev('i.clearable');
|
|
|
|
// 是否可清除已输入的内容(添加清除按钮)
|
|
if (options.clearable && !$iClear.length) {
|
|
$iClear = $('<i class="clearable glyphicon glyphicon-remove fa fa-plus"></i>')
|
|
.prependTo($input.parent());
|
|
}
|
|
|
|
return $iClear.css({
|
|
position: 'absolute',
|
|
top: 'calc(50% - 6px)',
|
|
transform: 'rotate(45deg)',
|
|
// right: options.showBtn ? Math.max($input.next('.input-group-btn').width(), 33) + 2 : 12,
|
|
zIndex: 4,
|
|
cursor: 'pointer',
|
|
width: '14px',
|
|
lineHeight: '14px',
|
|
textAlign: 'center',
|
|
fontSize: 12
|
|
}).hide();
|
|
}
|
|
/**
|
|
* 默认的配置选项
|
|
* @type {Object}
|
|
*/
|
|
var defaultOptions = {
|
|
url: null, // 请求数据的 URL 地址
|
|
jsonp: null, // 设置此参数名,将开启jsonp功能,否则使用json数据结构
|
|
data: {
|
|
value: []
|
|
}, // 提示所用的数据,注意格式
|
|
indexId: 0, // 每组数据的第几个数据,作为input输入框的 data-id,设为 -1 且 idField 为空则不设置此值
|
|
indexKey: 0, // 每组数据的第几个数据,作为input输入框的内容
|
|
idField: '', // 每组数据的哪个字段作为 data-id,优先级高于 indexId 设置(推荐)
|
|
keyField: '', // 每组数据的哪个字段作为输入框内容,优先级高于 indexKey 设置(推荐)
|
|
|
|
/* 搜索相关 */
|
|
autoSelect: TRUE, // 键盘向上/下方向键时,是否自动选择值
|
|
allowNoKeyword: TRUE, // 是否允许无关键字时请求数据
|
|
getDataMethod: 'firstByUrl', // 获取数据的方式,url:一直从url请求;data:从 options.data 获取;firstByUrl:第一次从Url获取全部数据,之后从options.data获取
|
|
delayUntilKeyup: FALSE, // 获取数据的方式 为 firstByUrl 时,是否延迟到有输入时才请求数据
|
|
ignorecase: FALSE, // 前端搜索匹配时,是否忽略大小写
|
|
effectiveFields: [], // 有效显示于列表中的字段,非有效字段都会过滤,默认全部有效。
|
|
effectiveFieldsAlias: {}, // 有效字段的别名对象,用于 header 的显示
|
|
searchFields: [], // 有效搜索字段,从前端搜索过滤数据时使用,但不一定显示在列表中。effectiveFields 配置字段也会用于搜索过滤
|
|
twoWayMatch: TRUE, // 是否双向匹配搜索。为 true 即输入关键字包含或包含于匹配字段均认为匹配成功,为 false 则输入关键字包含于匹配字段认为匹配成功
|
|
multiWord: FALSE, // 以分隔符号分割的多关键字支持
|
|
separator: ',', // 多关键字支持时的分隔符,默认为半角逗号
|
|
delay: 300, // 搜索触发的延时时间间隔,单位毫秒
|
|
emptyTip: '', // 查询为空时显示的内容,可为 html
|
|
searchingTip: '搜索中...', // ajax 搜索时显示的提示内容,当搜索时间较长时给出正在搜索的提示
|
|
hideOnSelect: FALSE, // 鼠标从列表单击选择了值时,是否隐藏选择列表
|
|
|
|
/* UI */
|
|
autoDropup: FALSE, // 选择菜单是否自动判断向上展开。设为 true,则当下拉菜单高度超过窗体,且向上方向不会被窗体覆盖,则选择菜单向上弹出
|
|
autoMinWidth: FALSE, // 是否自动最小宽度,设为 false 则最小宽度不小于输入框宽度
|
|
showHeader: FALSE, // 是否显示选择列表的 header。为 true 时,有效字段大于一列则显示表头
|
|
showBtn: TRUE, // 是否显示下拉按钮
|
|
inputBgColor: '', // 输入框背景色,当与容器背景色不同时,可能需要该项的配置
|
|
inputWarnColor: 'rgba(255,0,0,.1)', // 输入框内容不是下拉列表选择时的警告色
|
|
listStyle: {
|
|
'padding-top': 0,
|
|
'max-height': '375px',
|
|
'max-width': '800px',
|
|
'overflow': 'auto',
|
|
'width': 'auto',
|
|
'transition': '0.3s',
|
|
'-webkit-transition': '0.3s',
|
|
'-moz-transition': '0.3s',
|
|
'-o-transition': '0.3s',
|
|
'word-break': 'keep-all',
|
|
'white-space': 'nowrap'
|
|
}, // 列表的样式控制
|
|
listAlign: 'left', // 提示列表对齐位置,left/right/auto
|
|
listHoverStyle: 'background: #07d; color:#fff', // 提示框列表鼠标悬浮的样式
|
|
listHoverCSS: 'jhover', // 提示框列表鼠标悬浮的样式名称
|
|
clearable: FALSE, // 是否可清除已输入的内容
|
|
|
|
/* key */
|
|
keyLeft: 37, // 向左方向键,不同的操作系统可能会有差别,则自行定义
|
|
keyUp: 38, // 向上方向键
|
|
keyRight: 39, // 向右方向键
|
|
keyDown: 40, // 向下方向键
|
|
keyEnter: 13, // 回车键
|
|
|
|
/* methods */
|
|
fnProcessData: processData, // 格式化数据的方法,返回数据格式参考 data 参数
|
|
fnGetData: getData, // 获取数据的方法,无特殊需求一般不作设置
|
|
fnAdjustAjaxParam: null, // 调整 ajax 请求参数方法,用于更多的请求配置需求。如对请求关键字作进一步处理、修改超时时间等
|
|
fnPreprocessKeyword: null, // 搜索过滤数据前,对输入关键字作进一步处理方法。注意,应返回字符串
|
|
fnAjaxFail: null, // ajax 失败时回调方法
|
|
};
|
|
|
|
var methods = {
|
|
init: function(options) {
|
|
// 参数设置
|
|
var self = this;
|
|
options = options || {};
|
|
|
|
// 默认配置有效显示字段多于一个,则显示列表表头,否则不显示
|
|
if (isUndefined(options.showHeader) && options.effectiveFields && options.effectiveFields.length > 1) {
|
|
options.showHeader = TRUE;
|
|
}
|
|
|
|
options = $.extend(TRUE, {}, defaultOptions, options);
|
|
|
|
// 旧的方法兼容
|
|
if (options.processData) {
|
|
options.fnProcessData = options.processData;
|
|
}
|
|
|
|
if (options.getData) {
|
|
options.fnGetData = options.getData;
|
|
}
|
|
|
|
if (options.getDataMethod === 'firstByUrl' && options.url && !options.delayUntilKeyup) {
|
|
ajax(options).done(function(result) {
|
|
options.url = null;
|
|
self.trigger(onDataRequestSuccess, result);
|
|
});
|
|
}
|
|
|
|
// 鼠标滑动到条目样式
|
|
if (!$('#' + BSSUGGEST).length) {
|
|
$('head:eq(0)').append('<style id="' + BSSUGGEST + '">.' + options.listHoverCSS + '{' + options.listHoverStyle + '}</style>');
|
|
}
|
|
|
|
return self.each(function() {
|
|
var $input = $(this),
|
|
$parent = $input.parent(),
|
|
$iClear = getIClear($input, options),
|
|
isMouseenterMenu,
|
|
keyupTimer, // keyup 与 input 事件延时定时器
|
|
$dropdownMenu = $parent.find('ul:eq(0)');
|
|
|
|
// 兼容 bs4
|
|
$dropdownMenu.parent().css('position', 'relative');
|
|
|
|
// 验证输入框对象是否符合条件
|
|
if (!checkInput($input, $dropdownMenu, options)) {
|
|
console.warn('不是一个标准的 bootstrap 下拉式菜单或已初始化:', $input);
|
|
return;
|
|
}
|
|
|
|
// 是否显示 button 按钮
|
|
if (!options.showBtn) {
|
|
$input.css('borderRadius', 4);
|
|
$parent.css('width', '100%')
|
|
.find('.btn:eq(0)').hide();
|
|
}
|
|
|
|
// 移除 disabled 类,并禁用自动完成
|
|
$input.removeClass(DISABLED).prop(DISABLED, FALSE).attr('autocomplete', 'off');
|
|
// dropdown-menu 增加修饰
|
|
$dropdownMenu.css(options.listStyle);
|
|
|
|
// 默认背景色
|
|
if (!options.inputBgColor) {
|
|
options.inputBgColor = $input.css('backgroundColor');
|
|
}
|
|
|
|
// 开始事件处理
|
|
$input.on('keydown', function(event) {
|
|
var currentList, tipsKeyword; // 提示列表上被选中的关键字
|
|
|
|
// 当提示层显示时才对键盘事件处理
|
|
if (!$dropdownMenu.is(':visible')) {
|
|
setOrGetDataId($input, '');
|
|
return;
|
|
}
|
|
|
|
currentList = $dropdownMenu.find('.' + options.listHoverCSS);
|
|
tipsKeyword = ''; // 提示列表上被选中的关键字
|
|
|
|
unHoverAll($dropdownMenu, options);
|
|
|
|
if (event.keyCode === options.keyDown) { // 如果按的是向下方向键
|
|
if (!currentList.length) {
|
|
// 如果提示列表没有一个被选中,则将列表第一个选中
|
|
tipsKeyword = getPointKeyword($dropdownMenu.find('tbody tr:first').mouseover());
|
|
} else if (!currentList.next().length) {
|
|
// 如果是最后一个被选中,则取消选中,即可认为是输入框被选中,并恢复输入的值
|
|
if (options.autoSelect) {
|
|
setOrGetDataId($input, '').val(setOrGetAlt($input));
|
|
}
|
|
} else {
|
|
// 选中下一行
|
|
tipsKeyword = getPointKeyword(currentList.next().mouseover());
|
|
}
|
|
// 控制滑动条
|
|
adjustScroll($input, $dropdownMenu, options);
|
|
|
|
if (!options.autoSelect) {
|
|
return;
|
|
}
|
|
} else if (event.keyCode === options.keyUp) { // 如果按的是向上方向键
|
|
if (!currentList.length) {
|
|
tipsKeyword = getPointKeyword($dropdownMenu.find('tbody tr:last').mouseover());
|
|
} else if (!currentList.prev().length) {
|
|
if (options.autoSelect) {
|
|
setOrGetDataId($input, '').val(setOrGetAlt($input));
|
|
}
|
|
} else {
|
|
// 选中前一行
|
|
tipsKeyword = getPointKeyword(currentList.prev().mouseover());
|
|
}
|
|
|
|
// 控制滑动条
|
|
adjustScroll($input, $dropdownMenu, options);
|
|
|
|
if (!options.autoSelect) {
|
|
return;
|
|
}
|
|
} else if (event.keyCode === options.keyEnter) {
|
|
tipsKeyword = getPointKeyword(currentList);
|
|
hideDropMenu($input, options);
|
|
} else {
|
|
setOrGetDataId($input, '');
|
|
}
|
|
|
|
// 设置值 tipsKeyword
|
|
// console.log(tipsKeyword);
|
|
setValue($input, tipsKeyword, options);
|
|
}).on('compositionstart', function(event) {
|
|
// 中文输入开始,锁定
|
|
// console.log('compositionstart');
|
|
inputLock = TRUE;
|
|
}).on('compositionend', function(event) {
|
|
// 中文输入结束,解除锁定
|
|
// console.log('compositionend');
|
|
inputLock = FALSE;
|
|
}).on('keyup input paste', function(event) {
|
|
var word;
|
|
|
|
if (event.keyCode) {
|
|
setBackground($input, options);
|
|
}
|
|
|
|
// 如果弹起的键是回车、向上或向下方向键则返回
|
|
if (~$.inArray(event.keyCode, [options.keyDown, options.keyUp, options.keyEnter])) {
|
|
$input.val($input.val()); // 让鼠标输入跳到最后
|
|
return;
|
|
}
|
|
|
|
clearTimeout(keyupTimer);
|
|
keyupTimer = setTimeout(function() {
|
|
// console.log('input keyup', event);
|
|
|
|
// 锁定状态,返回
|
|
if (inputLock) {
|
|
return;
|
|
}
|
|
|
|
word = $input.val();
|
|
|
|
// 若输入框值没有改变则返回
|
|
if ($.trim(word) && word === setOrGetAlt($input)) {
|
|
return;
|
|
}
|
|
|
|
// 当按下键之前记录输入框值,以方便查看键弹起时值有没有变
|
|
setOrGetAlt($input, word);
|
|
|
|
if (options.multiWord) {
|
|
word = word.split(options.separator).reverse()[0];
|
|
}
|
|
|
|
// 是否允许空数据查询
|
|
if (!word.length && !options.allowNoKeyword) {
|
|
return;
|
|
}
|
|
|
|
options.fnGetData($.trim(word), $input, refreshDropMenu, options);
|
|
}, options.delay || 300);
|
|
}).on('focus', function() {
|
|
// console.log('input focus');
|
|
adjustDropMenuPos($input, $dropdownMenu, options);
|
|
}).on('blur', function() {
|
|
if (!isMouseenterMenu) { // 不是进入下拉列表状态,则隐藏列表
|
|
hideDropMenu($input, options);
|
|
}
|
|
}).on('click', function() {
|
|
// console.log('input click');
|
|
var word = $input.val();
|
|
|
|
if (
|
|
$.trim(word) &&
|
|
word === setOrGetAlt($input) &&
|
|
$dropdownMenu.find('table tr').length
|
|
) {
|
|
return showDropMenu($input, options);
|
|
}
|
|
|
|
if ($dropdownMenu.is(':visible')) {
|
|
return;
|
|
}
|
|
|
|
if (options.multiWord) {
|
|
word = word.split(options.separator).reverse()[0];
|
|
}
|
|
|
|
// 是否允许空数据查询
|
|
if (!word.length && !options.allowNoKeyword) {
|
|
return;
|
|
}
|
|
|
|
// console.log('word', word);
|
|
options.fnGetData($.trim(word), $input, refreshDropMenu, options);
|
|
});
|
|
|
|
// 下拉按钮点击时
|
|
$parent.find('.btn:eq(0)').attr('data-toggle', '').click(function() {
|
|
if (!$dropdownMenu.is(':visible')) {
|
|
if (options.url) {
|
|
$input.click().focus();
|
|
if (!$dropdownMenu.find('tr').length) {
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
// 不以 keyword 作为过滤,展示所有的数据
|
|
refreshDropMenu($input, options.data, options);
|
|
}
|
|
showDropMenu($input, options);
|
|
} else {
|
|
hideDropMenu($input, options);
|
|
}
|
|
|
|
return FALSE;
|
|
});
|
|
|
|
// 列表中滑动时,输入框失去焦点
|
|
$dropdownMenu.mouseenter(function() {
|
|
// console.log('mouseenter')
|
|
isMouseenterMenu = 1;
|
|
$input.blur();
|
|
}).mouseleave(function() {
|
|
// console.log('mouseleave')
|
|
isMouseenterMenu = 0;
|
|
$input.focus();
|
|
}).on('mouseenter', 'tbody tr', function() {
|
|
// 行上的移动事件
|
|
unHoverAll($dropdownMenu, options);
|
|
$(this).addClass(options.listHoverCSS);
|
|
|
|
return FALSE; // 阻止冒泡
|
|
})
|
|
.on('mousedown', 'tbody tr', function() {
|
|
var keywords = getPointKeyword($(this));
|
|
setValue($input, keywords, options);
|
|
setOrGetAlt($input, keywords.key);
|
|
setBackground($input, options);
|
|
|
|
if (options.hideOnSelect) {
|
|
hideDropMenu($input, options);
|
|
}
|
|
});
|
|
|
|
// 存在清空按钮
|
|
if ($iClear.length) {
|
|
$iClear.click(function () {
|
|
setOrGetDataId($input, '').val('');
|
|
setBackground($input, options);
|
|
});
|
|
|
|
$parent.mouseenter(function() {
|
|
if (!$input.prop(DISABLED)) {
|
|
$iClear.css('right', options.showBtn ? Math.max($input.next().width(), 33) + 2 : 12)
|
|
.show();
|
|
}
|
|
}).mouseleave(function() {
|
|
$iClear.hide();
|
|
});
|
|
}
|
|
|
|
});
|
|
},
|
|
show: function() {
|
|
return this.each(function() {
|
|
$(this).click();
|
|
});
|
|
},
|
|
hide: function() {
|
|
return this.each(function() {
|
|
hideDropMenu($(this));
|
|
});
|
|
},
|
|
disable: function() {
|
|
return this.each(function() {
|
|
$(this).attr(DISABLED, TRUE)
|
|
.parent().find('.btn:eq(0)').prop(DISABLED, TRUE);
|
|
});
|
|
},
|
|
enable: function() {
|
|
return this.each(function() {
|
|
$(this).attr(DISABLED, FALSE)
|
|
.parent().find('.btn:eq(0)').prop(DISABLED, FALSE);
|
|
});
|
|
},
|
|
destroy: function() {
|
|
return this.each(function() {
|
|
$(this).off().removeData(BSSUGGEST).removeAttr('style')
|
|
.parent().find('.btn:eq(0)').off().show().attr('data-toggle', 'dropdown').prop(DISABLED, FALSE) // .addClass(DISABLED);
|
|
.next().css('display', '').off();
|
|
});
|
|
},
|
|
version: function() {
|
|
return VERSION;
|
|
}
|
|
};
|
|
|
|
$.fn[BSSUGGEST] = function(options) {
|
|
// 方法判断
|
|
if (typeof options === 'string' && methods[options]) {
|
|
var inited = TRUE;
|
|
this.each(function() {
|
|
if (!$(this).data(BSSUGGEST)) {
|
|
return inited = FALSE;
|
|
}
|
|
});
|
|
// 只要有一个未初始化,则全部都不执行方法,除非是 init 或 version
|
|
if (!inited && 'init' !== options && 'version' !== options) {
|
|
return this;
|
|
}
|
|
|
|
// 如果是方法,则参数第一个为函数名,从第二个开始为函数参数
|
|
return methods[options].apply(this, [].slice.call(arguments, 1));
|
|
} else {
|
|
// 调用初始化方法
|
|
return methods.init.apply(this, arguments);
|
|
}
|
|
}
|
|
});
|
|
|