UNPKG

lu2

Version:

Simple and flexible UI component library based on native HTML and JavaScript

755 lines (661 loc) 19 kB
/** * @Datalist.js * @author xunxuzhang * @version * Created: 16-03-28 * **/ (function (global, factory) { if (typeof define === 'function' && (define.amd || define.cmd)) { define(factory); } else { global.Datalist = factory(); } }(this, function (require, exports, module) { if (typeof require == 'function') { require('common/ui/Follow'); } else if (!$().follow) { if (window.console) { console.error('need Follow.js'); } return {}; } /** * 数据下拉列表 * 类似于传统Autocomplete功能 * 仿HTML5原生datalist功能实现 * 支持静态呈现(基于现有HTML构建)和动态获取(本地存储或请求) */ // 常量变量 var DATALIST = 'datalist', SELECTED = 'selected', ACTIVE = 'active', REVERSE = 'reverse'; var CL = 'ui-datalist'; /* 过滤HTML标签的方法 */ $.stripHTML = function(str) { if (typeof str == 'string') { return str.replace(/<\/?[^>]*>/g, ''); } return ''; }; /* 转义HTML标签的方法 */ $.encodeHTML = function(str) { if (typeof str == 'string') { return str.replace(/<|&|>/g, function(matchs) { return ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' })[matchs]; }); } return ''; }; $.decodeHTML = function(str) { if (typeof str == 'string') { return str.replace(/&lt;|&gt;|&amp;/g, function(matchs) { return ({ '&lt;': '<', '&gt;': '>', '&amp;': '&' })[matchs]; }); } return ''; }; /* * 支持jQuery API调用和模块化调用两种 * @example * $('input').datalist(); * or * new Datalist($('input')) */ $.fn.datalist = function(options) { return $(this).each(function(index, element) { var datalist = $(this).data(DATALIST); if (!datalist) { $(this).data(DATALIST, new Datalist($(this), options)); } }); }; var Datalist = function(el, options) { if (!el) return; // el一般特指输入框 if (el.nodeType == 1) el = $(el); // 分3种情况 // 1. list属性静态获取(使用场景有限,已经不考虑该实现) // 2. 根据name值本地获取 // 3. options的data参数获取 var defaults = { // 列表数据,'auto'表示取自本地存储,还支持数组和函数类型和对象 data: 'auto', // 最多出现的匹配条目数,如果数目不限,使用'auto'关键字 max: 8, // 下拉列表的宽度,默认和输入框宽度一致,支持数值和function width: 'auto', // 显示容器 container: $(document.body), // 对总数据的过滤方法,默认前序匹配 filter: function(data, value) { // this是当前实例对象 var self = this; // 元素 var trigger = self.el.trigger; // this是当前实例对象 var arr = []; // 默认是从头到尾完全字符匹配 if ($.isArray(data)) { $.each(data, function(i, obj) { if (value == '' || obj.value.indexOf(value) == 0) { arr.push(obj); } }); } return arr; }, // 当值通过下拉选择改变时候触发的事件 // 可以数值,也可以是字符串 trigger: ['change'], // 显示和隐藏时候的回调 // this为实例对象,支持两个参数, // 分别是trigger(输入框)和target(生成列表)元素 onShow: $.noop, onHide: $.noop }; options = options || {}; // max是否通过原生的results属性设置 var results = el.attr('results'); if (results) { options.max = +results; } var params = $.extend({}, defaults, options); // 暴露的数据 var self = this; self.el = { input: el, trigger: el, container: params.container }; self.callback = { trigger: params.trigger, filter: params.filter, show: params.onShow, hide: params.onHide }; self.params = { width: params.width, max: params.max, data: params.data, index: -1 }; this.bool = { modern: 'oninput' in document.createElement('div') }; self.display = false; // 从本地自动获得数据 if (params.data == 'auto') { // 前提要有name属性值同时autocomplete开启,也就是值不是off var name = el.attr('name'), autocomplete = el.attr('autocomplete'); if (name && autocomplete != 'off') { // 跟随浏览器autocomplete规范实现 // 首先灭了浏览器内置的autocomplete el.attr('autocomplete', 'off'); } this.callback.data = function() { var data = []; // IE7忽略自动记忆功能 if (window.localStorage && name && autocomplete != 'off') { // 本地获取 var strList = localStorage[DATALIST + '-' + name]; if (strList) { $.each(strList.split(','), function(i, value) { // value必须 if (value) data.push({ label: '', value: value }); }); } } return data; }; // 表单记忆 el.parents('form').on('submit', function() { self.store(); }); } // 直接data参数 else if ($.isArray(params.data)) { this.callback.data = function() { return params.data; }; } // 如果是函数 else if ($.isFunction(params.data)) { this.callback.data = params.data; } // 如果是ajax参数对象 else if ($.isPlainObject(params.data) && params.data.url) { // 首先灭了浏览器内置的autocomplete el.attr('autocomplete', 'off'); // 更改filter内置过滤处理(如果用户并没有自定义的话) if (!options.filter) { self.callback.filter = function(data) { return data; }; } // 外部的success设置 var customSuccess = params.data.success; this.callback.data = function() { // 清除延时,避免妹子输入都请求 clearTimeout(self.ajaxTimer); // 有2个参数有内置,需要合并 // 1是搜索查询参数 // 2是成功后的回调 var ajaxParams = $.extend({}, { dataType: 'json' }, params.data); ajaxParams.data = ajaxParams.data || {}; // 合并搜索查询 var name = el.attr('name') || 'k', value = $.trim(el.val()); if (value == '') { self.data = []; return []; } ajaxParams.data[name] = value; // 合并成功回调 ajaxParams.success = function(json) { if (customSuccess) { customSuccess.apply(ajaxParams, arguments); } if (json && $.isArray(json.data)) { self.refresh(self.callback.filter.call(self, json.data)); } }; // 请求保护,200毫秒延迟判断 self.ajaxTimer = setTimeout(function() { $.ajax(ajaxParams); }, 200); }; } // 上面的数据方法准备完毕,下面事件 this.events(); }; /* * 本地存储输入框的值 */ Datalist.prototype.store = function() { // 元素 var trigger = this.el.trigger; // 元素属性值 var value = '', name = ''; // 只有data为auto时候才本地记忆 if (window.localStorage && this.params.data == 'auto' && (value = this.value()) && (name = trigger.attr('name'))) { // 本地获取 var arrList = (localStorage[DATALIST + '-' + name] || '').split(','); // 如果当前数据并未保存过 var indexMatch = $.inArray(value, arrList); if (indexMatch == -1) { // 新值,前面插入 arrList.unshift(value); } else if (indexMatch != 0) { // 如果当前匹配内容不是首位,顺序提前 // 把需要提前的数组弄出来 var arrSplice = arrList.splice(indexMatch, 1); // 重新连接 arrList = arrSplice.concat(arrList); } // 更改对应的本地存储值 localStorage[DATALIST + '-' + name] = arrList.join(); } return this; }, /* * 清除本地存储的值 */ Datalist.prototype.removeStore = function(value) { // value参数存在下面3种逻辑 // 1. 字符串内容,表示移除本地该值数据(如果有) // 2. true,表示清空本地对应该存储 // 3. undefined, 表示使用trigger的value值作为移除对象 // 元素 var trigger = this.el.trigger; // 元素属性值 var name = trigger.attr('name'); // 只有data为auto时候才本地记忆 if (window.localStorage && this.params.data == 'auto' && name && (value = value || this.value())) { if (value === true) { localStorage.removeItem(DATALIST + '-' + name); } else if (typeof value == 'string') { // 本地获取 var arrList = (localStorage[DATALIST + '-' + name] || '').split(','); // 当前数据位置 var indexMatch = $.inArray(value, arrList); if (indexMatch != -1) { // 删除 arrList.splice(indexMatch, 1); // 更改对应的本地存储值 localStorage[DATALIST + '-' + name] = arrList.join(); } } } return this; }, /* * 刷新列表 */ Datalist.prototype.refresh = function(data) { var self = this; // 元素们 var trigger = this.el.trigger, target = this.el.target; if (!target) { this.create(); target = this.el.target; } // 此时输入框的值 var value = this.value(); // 列表的刷新 // 根据data和filter得到最终呈现的数据 if (typeof data == 'undefined') { var data = this.callback.filter.call(this, this.callback.data(), value); if ($.isArray(data) == false) return this; } // 显示的最大个数 if (typeof this.params.max == 'number') { data = data.slice(0, this.params.max); } // 暴露最终使用的列表数据 this.data = data; // 列表HTML组装 var htmlList = ''; if (data && data.length) { // 先不匹配任何列表项 self.params.index = -1; // 拼接列表字符内容 $.each(data, function(i, obj) { // 过滤HTML标签 var valueStrip = $.stripHTML(obj.value || ''), labelStrip = $.stripHTML(obj.label || ''); var selected = ''; if (value && valueStrip == value) { selected = ' ' + SELECTED; // 这个键盘操作时候需要 self.params.index = i; } if (obj.label) { htmlList = htmlList + // 虽然使用其他标签模拟<datalist> // 但是,一些属性值,还是遵循HTML规范 '<li class="'+ CL +'-option'+ selected +'" value="'+ valueStrip +'" label="'+ labelStrip +'" data-index="'+ i +'">'+ // label应该前置,可以通过相邻选择器控制后面内容的UI '<label class="'+ CL +'-label">'+ obj.label + '</label><span class="'+ CL +'-value">'+ obj.value + '</span>' + '</li>'; } else { htmlList = htmlList + // 但是,一些属性值,还是遵循HTML规范 '<li class="'+ CL +'-option'+ selected +'" value="'+ valueStrip +'" data-index="'+ i +'">'+ '<span class="'+ CL +'-value">'+ obj.value + '</span>' + '</li>'; } }); } if (htmlList != '') { htmlList = '<ul class="'+ CL +'-datalist">' + htmlList + '</ul>'; } target.html(htmlList); if (htmlList) { if (this.display == false) { this.show(); } } else if (this.display == true) { this.hide(); } if (this.display == true) { var eleSelected = target.find('.' + SELECTED)[0]; if (eleSelected) { $(eleSelected).parent().scrollTop(eleSelected.offsetTop); } } }; Datalist.prototype.create = function() { var self = this; // list属性值需要和创建的列表元素id对应获取 var trigger = this.el.trigger; // 原生HTML5应该是对应datalist元素 // 但1. datalist无法自定义UI; 2. IE9-不支持;3. 一些bug存在 // 所以,我们使用普通的ul列表元素模拟 if (!this.el.target) { // 看看是否有list属性值 var id = trigger.attr('list'); if (!id) { // 如果没有关联id,创建之 id = DATALIST + (Math.random() + '').split('.')[1]; trigger.attr('list', id); } var target = $('<div id="'+ id +'">').addClass(CL).on('click', 'li', function() { // 选择某一项 var value = $(this).attr('value'); // 赋值 self.value(value) // 关闭列表 .hide(); }); if (trigger.attr('id')) { target.addClass(DATALIST + '-' + trigger.attr('id').toLowerCase()); } // 载入页面 self.el.container.append(target); // 元素暴露 this.el.target = target; // 默认display态 this.display = false; } return this; }; /* * 输入框赋值或者取值 */ Datalist.prototype.value = function(value) { // 元素们 var trigger = this.el.trigger; // 回调 var triggerEvent = this.callback.trigger, // 常量变量 INPUT = 'input'; if (typeof value == 'undefined') { // 取值 return $.encodeHTML($.trim(trigger.val())); } else { // 赋值 trigger.val($.decodeHTML(value + '')); } // 事件 if (value && triggerEvent) { // 赋值时候触发的回调事件们 // 对于现代浏览器input事件是一定要触发的 if (this.bool.modern) { if ($.isArray(triggerEvent)) { if ($.inArray(INPUT, triggerEvent) == -1) { triggerEvent.push(INPUT); } } else if (triggerEvent && typeof triggerEvent == 'string' && triggerEvent != INPUT) { triggerEvent = [INPUT, triggerEvent]; } else { triggerEvent = [INPUT]; } } else // IE7, IE8赋值自动触发,无需额外触发input if ($.isArray(triggerEvent) == false) { triggerEvent = [triggerEvent]; } $.each(triggerEvent, function(i, eventType) { trigger.trigger(eventType); }); } return this; }, /* * 一些事件 */ Datalist.prototype.events = function() { var self = this; // 事件 // 元素们 var trigger = this.el.trigger; // 输入 trigger.on({ blur: function() { // 仅仅标记当前是否是focus态 $(this).data('focus', false); }, click: function() { if (self.display == false && $(this).data('focus') == true && (self.params.data == 'auto' || $.trim(this.value)) ) { // 当前列表隐藏,同时是focus态 self.refresh(); } // 标记为focus态 $(this).data('focus', true); }, input: function(event) { if (event.isTrigger) return; // 输入行为的时候,如果内容为空,内隐藏列表 if ($.trim(this.value)) { self.refresh(); } else { self.hide(); } }, keydown: function(event) { // data为当前列表使用的数据 // index表示当前选中列表的索引值 var data = self.data, index = self.params.index; switch (event.keyCode) { case 27: case 13: { // ESC-27 ENTER-13 if (self.display == true) { // 列表隐藏 self.hide(); event.preventDefault(); if (this.value && self.el.target.find('.' + SELECTED).length) { // 当键盘选值的时候,阻止默认行为 // 例如ENTER表单提交,ESC输入框内容清空 event.preventDefault(); // 文本内容从选中态改为focus态 setTimeout(function() { var ele = event.target; if(ele.setSelectionRange) { try { // 部分输入框类型,例如email, number不支持selection ele.setSelectionRange(ele.value.length, ele.value.length); } catch (e) { ele.value = ele.value; } } else { ele.value = ele.value; } // 触发Validate.js中的验证 if (isSupportInput) $(ele).trigger('input'); }, 17); } } break; } case 38: { // UP-38 if (self.display == true && data && data.length) { index--; if (index < 0) { index = data.length - 1; } } break; } case 40: { // DOWN-40 if (self.display == true && data && data.length) { index++; if (index > data.length - 1) { index = 0; } } break; } } if (event.keyCode == 38 || event.keyCode == 40) { event.preventDefault(); // for IE8 不执行onpropertychange事件 $(this).data('stopPropertyChange', true) // 上下键的时候,列表数据不动态获取和过滤 if (data[index]) self.value($.stripHTML(data[index].value)); $(this).select(); self.refresh(data); } } }); var isSupportInput = this.bool.modern; // IE7-IE8 模拟 input if (trigger.length && isSupportInput == false) { trigger[0].attachEvent('onpropertychange', function(event) { if (event && event.propertyName == "value" && !trigger.data('stopPropertyChange')) { if ($.trim(trigger.val())) { self.refresh(); } else { self.hide(); } } $(this).data('stopPropertyChange', false) }); } // 点击空白处隐藏 $(document).mouseup(function(event) { var ele = event.target, target = self.el.target; if (ele && target && ele != trigger[0] && ele != target[0] && target[0].contains(ele) == false) { self.hide(); } }); // 浏览器窗口改变重定位 $(window).resize(function() { if (self.display == true) { self.position(); } }); return this; }; Datalist.prototype.position = function() { // 元素们 var trigger = this.el.trigger, target = this.el.target; if (trigger && target) { target.follow(trigger, { // 边缘不自动调整,此处手动调整 edgeAdjust: false }); if (this.display == true) { trigger.addClass(ACTIVE); } } // 列表的定位 return this; }; Datalist.prototype.show = function() { // 元素们 var trigger = this.el.trigger, target = this.el.target; if (!target) { this.create(); target = this.el.target; } // 当前的显示状态 var display = this.display; // 列表的宽度 var width = this.params.width; if (width == 'auto') { width = trigger.outerWidth(); } else if ($.isFunction(width)) { width = width.call(this, trigger, target); } if (width != 'auto' && typeof width != 'number') { width = trigger.width(); } // z-index自动最高 var zIndex = target.css('zIndex') * 1 || 19, maxIndex = zIndex; $('body').children().each(function() { var z = $(this).css('zIndex') * 1; if (z) { maxIndex = Math.max(z, maxIndex); } }); if (zIndex < maxIndex) { target.css('zIndex', maxIndex + 1); } target.css({ display: 'block', width: width }); // 显示状态标记 this.display = true; // 定位 this.position(); // 显示回调,当之前隐藏时候才触发 if (display == false) { this.callback.show.call(this, trigger, target); } }; Datalist.prototype.hide = function() { // 元素们 var trigger = this.el.trigger, target = this.el.target; if (target && this.display == true) { target.hide().removeClass(REVERSE); // 隐藏回调 this.callback.hide.call(this, trigger, target); } trigger.removeClass(ACTIVE).removeClass(REVERSE); // 隐藏状态标记 this.display = false; }; return Datalist; }));