lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
836 lines (732 loc) • 31.4 kB
JavaScript
/**
* @Select.js
* @author zhangxinxu
* @version
* @Created: 15-06-18
* @edit: 07-06-15 rewrite
* @edit: 09-08-28 native js rewrite
* @edit: 21-12-02 append to body for re-position
**/
/* global module, global */
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && (define.amd || define.cmd)) {
define(factory);
} else {
global.Select = factory();
}
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function () {
/**
* 模拟下拉框效果
* 针对所有浏览器进行处理
* 基于原生的<select>元素生成
*
*/
// 常量变量
var SELECT = 'select';
var SELECTED = 'selected';
var DISABLED = 'disabled';
var ACTIVE = 'active';
var REVERSE = 'reverse';
var MULTIPLE = 'multiple';
// 样式类名统一处理
var CL = {
add: function () {
return ['ui', SELECT].concat([].slice.call(arguments)).join('-');
},
toString: function () {
return 'ui-' + SELECT;
}
};
/**
* 基于原生下拉框生成的下拉组件
* @param {Object} element 原生select元素
*/
var Select = function (element) {
if (!element) {
return this;
}
var eleSelect = element;
// 避免重复初始化
if (eleSelect.data && eleSelect.data.select) {
return;
}
var strAttrWidth = eleSelect.style.width || eleSelect.getAttribute('width');
if (!strAttrWidth) {
strAttrWidth = eleSelect.offsetWidth;
}
this.offsetWidth = strAttrWidth;
// 构造元素
// 1. 得到关联id
var strId = eleSelect.id;
if (!strId) {
strId = ('lulu_' + Math.random()).replace('0.', '');
} else {
strId = 'lulu_' + strId;
}
this.id = strId;
// 2. 是否多选
var isMultiple = (typeof eleSelect.getAttribute(MULTIPLE) == 'string');
this.multiple = isMultiple;
// 3. 创建下拉组合框元素
var eleCombobox = document.createElement('div');
// 4. 创建下拉点击按钮元素
var eleButton = document.createElement('a');
if (!eleSelect[DISABLED]) {
eleButton.setAttribute('href', 'javascript:');
}
eleButton.setAttribute('data-target', strId);
// 下面3个aria无障碍访问需要
eleButton.setAttribute('role', 'button');
eleButton.setAttribute('aria-expanded', 'false');
eleButton.setAttribute('aria-owns', strId);
// 样式类名
eleButton.classList.add(CL.add('button'));
// 5. 创建下拉列表元素
var eleDatalist = document.createElement('ui-select-list');
eleDatalist.id = strId;
eleDatalist.setAttribute('role', 'listbox');
eleDatalist.setAttribute('aria-expanded', 'true');
eleDatalist.classList.add(CL.add('datalist'));
// 6. 元素组装
// multiple没有button
if (isMultiple == false) {
eleCombobox.appendChild(eleButton);
// 插入到下拉框的后面
eleSelect.style.display = 'none';
eleSelect.insertAdjacentElement('afterend', eleCombobox);
} else {
eleCombobox.appendChild(eleDatalist);
// 绝对定位隐藏,以便可以响应键盘focus
eleSelect.style.position = 'absolute';
eleSelect.style.zIndex = 1;
eleSelect.insertAdjacentElement('afterend', eleCombobox);
// 视觉列表不参与设置为不可访问
eleDatalist.setAttribute('aria-hidden', 'true');
}
// 暴露给其他方法
this.element = {
select: eleSelect,
combobox: eleCombobox,
button: eleButton,
datalist: eleDatalist
};
// 刷新内容
this.refresh();
// 事件绑定
this.events();
// 存储
if (!eleSelect.data) {
eleSelect.data = {
select: this
};
} else {
eleSelect.data.select = this;
}
};
/**
* 下拉相关的事件处理
* @return {[type]} [description]
*/
Select.prototype.events = function () {
// 各个元素
var objElement = this.element;
// 主要的几个元素
var eleSelect = objElement.select;
var eleCombobox = objElement.combobox;
var eleButton = objElement.button;
var eleDatalist = objElement.datalist;
// 单选下拉框的事件
if (this.multiple == false) {
// 点击页面空白要隐藏
// 测试表明,这里优化下可以提高40~50%性能
// 原本是绑定对应元素上,现在改成委托
if (!document.isSelectMouseEvent) {
document.addEventListener('click', function (event) {
var target = event.target;
if (!target || !target.closest) {
return;
}
// 获取下拉元素是关键,因为存储了实例对象
// 元素什么的都可以直接匹配
eleCombobox = target.closest('.' + CL);
// 如果点击的是 body 定位的下拉列表元素
if (!eleCombobox) {
eleDatalist = target.closest('ui-select-list');
if (eleDatalist && eleDatalist.id) {
eleButton = document.querySelector('a[data-target="' + eleDatalist.id + '"]');
if (eleButton) {
eleCombobox = eleButton.closest('.' + CL);
}
}
}
eleSelect = eleCombobox && eleCombobox.previousElementSibling;
if (!eleSelect || !eleSelect.data || !eleSelect.data.select) {
return;
}
// 按钮和列表元素就知道了
objElement = eleSelect.data.select.element;
eleButton = objElement.button;
eleDatalist = objElement.datalist;
// 下面判断点击的是按钮还是列表了
if (eleButton.contains(target)) {
if (eleSelect[DISABLED]) {
return false;
}
// 显示与隐藏
eleCombobox.classList.toggle(ACTIVE);
if (eleCombobox.classList.contains(ACTIVE)) {
// 1. 为防止 overflow 剪裁下拉列表,改为 body 定位
// 2. 为防止默认状态下,大量的 DOM 作为 <body> 子元素呈现,改为交互后 append 处理
if (!document.body.contains(eleDatalist)) {
document.body.appendChild(eleDatalist);
}
// 使用 JS 计算定位,为减少依赖,就不引用 Follow.js 了
var objBoundButton = eleButton.getBoundingClientRect();
eleDatalist.classList.add(ACTIVE);
eleDatalist.style.left = (objBoundButton.left + window.pageXOffset) + 'px';
eleDatalist.style.top = (objBoundButton.bottom + window.pageYOffset - 1) + 'px';
eleDatalist.style.width = objBoundButton.width + 'px';
// 层级
this.zIndex();
// 边界判断
var objBoundDatalist = eleDatalist.getBoundingClientRect();
var isOverflow = objBoundDatalist.bottom + window.pageYOffset > Math.max(document.body.clientHeight, window.innerHeight);
// 如果超出边界
eleCombobox.classList[isOverflow ? 'add' : 'remove'](REVERSE);
if (isOverflow) {
eleDatalist.style.top = (objBoundButton.top + window.pageYOffset - objBoundDatalist.height + 1) + 'px';
}
// aria状态
eleButton.setAttribute('aria-expanded', 'true');
// 滚动与定位
var arrDataScrollTop = eleCombobox.dataScrollTop;
var eleDatalistSelected = eleDatalist.querySelector('.' + SELECTED);
// 严格验证
if (arrDataScrollTop && eleDatalistSelected && arrDataScrollTop[1] == eleDatalistSelected.getAttribute('data-index') && arrDataScrollTop[2] == eleDatalistSelected.innerText) {
eleDatalist.scrollTop = arrDataScrollTop[0];
// 重置
delete eleCombobox.dataScrollTop;
}
} else {
eleCombobox.classList.remove(REVERSE);
// aria状态
eleButton.setAttribute('aria-expanded', 'false');
// 隐藏列表
eleDatalist.remove();
}
} else if (eleDatalist.contains(target)) {
// 点击的列表元素
var eleList = target;
// 对应的下拉<option>元素
var eleOption = null;
// 是否当前点击列表禁用
var isDisabled = eleList.classList.contains(DISABLED);
// 获取索引
var indexOption = eleList.getAttribute('data-index');
// 存储可能的滚动定位需要的数据
var scrollTop = eleDatalist.scrollTop;
eleCombobox.dataScrollTop = [scrollTop, indexOption, eleList.innerText];
// 修改下拉选中项
if (isDisabled == false) {
eleOption = eleSelect[indexOption];
if (eleOption) {
eleOption[SELECTED] = true;
}
}
// 下拉收起
eleCombobox.classList.remove(ACTIVE);
eleButton.setAttribute('aria-expanded', 'false');
eleDatalist.remove();
// focus
eleButton.focus();
eleButton.blur();
if (isDisabled == false) {
// 更新下拉框
eleSelect.refresh();
// 回调处理
// 触发change事件
eleSelect.dispatchEvent(new CustomEvent('change', {
'bubbles': true
}));
}
}
}.bind(this));
document.addEventListener('mouseup', function (event) {
var target = event.target;
if (!target) {
return;
}
// 识别此时的combobox
eleCombobox = document.querySelector(SELECT + '+.' + CL + '.' + ACTIVE);
if (!eleCombobox) {
return;
}
eleButton = eleCombobox.querySelector('.' + CL.add('button'));
eleDatalist = document.getElementById(eleButton.getAttribute('data-target'));
if (eleDatalist && eleCombobox.contains(target) == false && eleDatalist.contains(target) == false) {
eleCombobox.classList.remove(ACTIVE);
eleCombobox.classList.remove(REVERSE);
eleDatalist.remove();
// aria状态
if (eleButton) {
eleButton.setAttribute('aria-expanded', 'false');
}
}
});
document.isSelectMouseEvent = true;
}
// disabled状态变化与键盘访问
var funSyncDisabled = function () {
if (eleSelect[DISABLED]) {
eleButton.removeAttribute('href');
} else {
eleButton.href = 'javascript:';
}
};
// 禁用状态变化检测
if (window.MutationObserver) {
var observerSelect = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) {
if (mutation.type == 'attributes') {
funSyncDisabled();
}
});
});
observerSelect.observe(eleSelect, {
attributes: true,
attributeFilter: [DISABLED]
});
} else {
eleSelect.addEventListener('DOMAttrModified', function (event) {
if (event.attrName == DISABLED) {
funSyncDisabled();
}
});
}
} else {
// 下拉多选
// 键盘交互UI同步
eleSelect.addEventListener('change', function () {
// 更新下拉框
this.refresh();
}.bind(this));
// 滚动同步
eleSelect.addEventListener('scroll', function () {
eleDatalist.scrollTop = eleSelect.scrollTop;
});
// hover穿透
eleSelect.addEventListener('mousedown', function () {
eleSelect.setAttribute('data-active', 'true');
});
eleSelect.addEventListener('mousemove', function (event) {
if (eleSelect.getAttribute('data-active')) {
this.refresh();
return;
}
var clientY = event.clientY;
// 当前坐标元素
// 最好方法是使用
// document.elementsFromPoint
// IE10+是document.msElementFromPoint
// 但IE8, IE9浏览器并不支持
// 所以这里采用其他方法实现
// 判断列表的y位置和clientY做比较
var eleListAll = eleDatalist.querySelectorAll('a');
for (var indexList = 0; indexList < eleListAll.length; indexList++) {
var eleList = eleListAll[indexList];
// hover状态先还原
eleList.removeAttribute('href');
// 然后开始寻找匹配的列表元素
// 进行比对
var beginY = eleList.getBoundingClientRect().top;
var endY = beginY + eleList.clientHeight;
// 如果在区间范围
if (clientY >= beginY && clientY <= endY) {
if (eleList.classList.contains(SELECTED) == false && eleList.classList.contains(DISABLED) == false) {
eleList.href = 'javascript:';
}
// 退出循环
// forEach无法中断,因此这里使用了for循环
break;
}
}
}.bind(this));
eleSelect.addEventListener('mouseout', function () {
var eleListAllWithHref = eleDatalist.querySelectorAll('a[href]');
eleListAllWithHref.forEach(function (eleList) {
eleList.removeAttribute('href');
});
});
document.addEventListener('mouseup', function () {
eleSelect.removeAttribute('data-active');
});
}
};
/**
* 把下拉元素变成数据,格式为:
* [{
html: '选项1',
value: '1',
selected: false,
className: 'a'
}, {
html: '选项2',
value: '2',
selected: true,
className: 'b'
}]
* @return 数组
*/
Select.prototype.getData = function () {
var eleSelect = this.element.select;
var eleOptions = eleSelect.querySelectorAll('option');
if (eleOptions.length == 0) {
return [{
html: ''
}];
}
// 所有分组元素
var eleOptgroups = eleSelect.querySelectorAll('optgroup');
// 如果有任意一个分组元素设置了label,那么就是标题分组
// 如果只是optgroup标签包括,那么使用分隔线分隔
var isIntent = !!eleSelect.querySelector('optgroup[label]');
// 如果有分组
if (eleOptgroups.length) {
var arrData = [];
eleOptgroups.forEach(function (optgroup) {
arrData.push({
html: optgroup.label,
disabled: optgroup.disabled,
className: optgroup.className,
hr: !isIntent
});
optgroup.querySelectorAll('option').forEach(function (option) {
arrData.push({
html: option.label || option.innerHTML,
value: option.value,
selected: option.selected,
disabled: optgroup.disabled || option.disabled,
className: option.className,
intent: isIntent
});
});
});
return arrData;
}
return [].slice.call(eleOptions).map(function (option) {
return {
html: option.label || option.innerHTML,
value: option.value,
selected: option.selected,
disabled: option.disabled,
className: option.className
};
});
};
/**
* 下拉刷新方法
* @param {Array} data 根据数组数据显示下拉内容
* @return {Object} 返回当前实例对象
*/
Select.prototype.refresh = function (data) {
// 实例id
var id = this.id;
// 是否多选
var isMultiple = this.multiple;
// 各个元素
var objElement = this.element;
// 主要的几个元素
var eleSelect = objElement.select;
var eleCombobox = objElement.combobox;
var eleButton = objElement.button;
var eleDatalist = objElement.datalist;
// 获取当下下拉框的数据和状态
data = data || this.getData();
// 下拉组合框元素的UI和尺寸
eleCombobox.className = (eleSelect.className + ' ' + CL).trim();
// offsetWidth/clientWidth/getBoundingClientRect在下拉元素很多的的时候会有明显的性能问题
// 因此宽度已知的时候,使用定值,否则实时获取
var strAttrWidth = this.offsetWidth;
if (/\D$/.test(strAttrWidth)) {
// 如果是<length>
eleCombobox.style.width = strAttrWidth;
} else {
// 如果是<number>
eleCombobox.style.width = strAttrWidth + 'px';
}
// 多选,高度需要同步,因为选项高度不确定
if (isMultiple) {
eleCombobox.style.height = eleSelect.style.height || (eleSelect.offsetHeight + 'px');
} else {
// 按钮元素中的文案
eleButton.innerHTML = '<span class="' + CL.add('text') + '">' + (function () {
var htmlSelected = '';
data.forEach(function (obj) {
if (obj.selected) {
htmlSelected = obj.html;
}
});
return htmlSelected || data[0].html;
})() + '</span><i class="' + CL.add('icon') + '" aria-hidden="true"></i>';
}
var index = -1;
// 列表内容的刷新
eleDatalist.innerHTML = data.map(function (obj) {
var arrCl = [CL.add('datalist', 'li')];
if (obj.className) {
arrCl.push(obj.className);
}
if (obj[SELECTED]) {
arrCl.push(SELECTED);
}
if (obj[DISABLED]) {
arrCl.push(DISABLED);
}
// 如果有分隔线
if (typeof obj.hr != 'undefined') {
if (obj.hr) {
arrCl = [CL.add('datalist', 'hr'), obj.className];
return '<div class="' + arrCl.join(' ') + '"></div>';
}
return '<div class="' + arrCl.join(' ') + '" role="heading">' + obj.html + '</div>';
}
// 这才是有效的索引
index++;
// 如果有缩进
if (obj.intent) {
arrCl.push(CL.add('intent'));
}
// 如果没有项目内容
if (!obj.html) {
arrCl.push(DISABLED);
return '<span class="' + arrCl.join(' ') + '"></span>';
}
// 复选模式列表不参与无障碍访问识别,因此HTML相对简单
if (isMultiple) {
return '<a class="' + arrCl.join(' ') + '" data-index=' + index + '>' + obj.html + '</a>';
}
return '<a ' + (obj[DISABLED] ? '' : 'href="javascript:" ') + 'class="' + arrCl.join(' ') + '" data-index=' + index + ' data-target="' + id + '" role="option" aria-selected="' + obj[SELECTED] + '">' + obj.html + '</a>';
}).join('');
return this;
};
/**
* 下拉列表元素zIndex实时最大化
* @return {[type]} [description]
*/
Select.prototype.zIndex = function () {
var eleDatalist = document.querySelector('body > ui-select-list.active');
// 返回eleTarget才是的样式计算对象
var objStyleTarget = window.getComputedStyle(eleDatalist);
// 此时元素的层级
var numZIndexTarget = objStyleTarget.zIndex;
// 用来对比的层级,也是最小层级
var numZIndexNew = 19;
// 只对<body>子元素进行层级最大化计算处理
document.body.childNodes.forEach(function (eleChild) {
if (eleChild.nodeType != 1) {
return;
}
var objStyleChild = window.getComputedStyle(eleChild);
var numZIndexChild = objStyleChild.zIndex * 1;
if (numZIndexChild && eleDatalist != eleChild && objStyleChild.display != 'none') {
numZIndexNew = Math.max(numZIndexChild + 1, numZIndexNew);
}
});
if (numZIndexNew != numZIndexTarget) {
eleDatalist.style.zIndex = numZIndexNew;
}
};
/**
* 删除方法
* @return {[type]} [description]
*/
Select.prototype.remove = function () {
// 主元素
var eleCombobox = this.element.combobox;
if (eleCombobox) {
eleCombobox.remove();
}
};
// 重新定义select元素的value方法
Object.defineProperty(HTMLSelectElement.prototype, 'value', {
configurable: true,
enumerable: true,
writeable: true,
get: function () {
var arrValue = [];
this.querySelectorAll('option').forEach(function (eleOption) {
if (eleOption[SELECTED] == true) {
arrValue.push(eleOption.value);
}
});
return arrValue.join();
},
set: function (value) {
var isOptionMatch = false;
// 是否多选框
var isMultiple = (typeof this.getAttribute('multiple') == 'string');
if (isMultiple) {
value = value.split(',');
} else {
value = [value.toString()];
}
this.querySelectorAll('option').forEach(function (eleOption) {
// 单选框模式下,如果多个值匹配,让第一个选中
// 如果没有下面这句,会最后一个匹配的选中
if (isMultiple == false && isOptionMatch == true) {
return;
}
if (value.indexOf(eleOption.value) != -1) {
eleOption[SELECTED] = isOptionMatch = true;
} else if (isMultiple) {
eleOption[SELECTED] = false;
}
});
// 如果包含匹配的值,则重新刷新
if (isOptionMatch == true) {
this.refresh();
}
}
});
HTMLSelectElement.prototype.refresh = function () {
if (this.data && this.data.select) {
this.data.select.refresh();
} else {
new Select(this);
}
};
var funAutoInitAndWatching = function () {
// 如果没有开启自动初始化,则返回
if (window.autoInit === false) {
return;
}
document.querySelectorAll('select').forEach(function (eleSelect) {
if (window.getComputedStyle(eleSelect).opacity != '1') {
eleSelect.refresh();
}
});
// 如果没有开启观察,不监听DOM变化
if (window.watching === false) {
return;
}
var funSyncRefresh = function (node, action) {
if (node.nodeType != 1) {
return;
}
if (node.tagName == 'SELECT') {
if (action == 'remove') {
if (node.data && node.data.select) {
node.data.select[action]();
} else {
node.parentNode.removeChild(node);
}
} else {
node[action]();
}
} else if (node.tagName == 'OPTION') {
var eleSelect = node.parentElement;
if (!eleSelect) {
// 可以认为是观察者模式的删除
if (this.target && this.target.tagName == 'SELECT') {
this.target.refresh();
}
} else if (eleSelect.data && eleSelect.data.select) {
setTimeout(function () {
eleSelect.refresh();
}, 16);
}
} else if (action == 'refresh') {
// 此时Select初始化也会触发DOM检测
// 但没有必要,因此,阻止
funAutoInitAndWatching.flag = false;
// 只是Select初始化
node.querySelectorAll('select').forEach(function (element) {
funSyncRefresh(element, action);
});
// 恢复到正常检测
funAutoInitAndWatching.flag = true;
}
};
// DOM Insert自动初始化
// IE11+使用MutationObserver
// IE9-IE10使用Mutation Events
if (window.MutationObserver) {
var observerSelect = new MutationObserver(function (mutationsList) {
// 此时不检测DOM变化
if (funAutoInitAndWatching.flag === false || window.watching === false) {
return;
}
mutationsList.forEach(function (mutation) {
var nodeAdded = mutation.addedNodes;
var nodeRemoved = mutation.removedNodes;
if (nodeAdded.length) {
nodeAdded.forEach(function (eleAdd) {
funSyncRefresh.call(mutation, eleAdd, 'refresh');
});
}
if (nodeRemoved.length) {
nodeRemoved.forEach(function (eleRemove) {
funSyncRefresh.call(mutation, eleRemove, 'remove');
});
}
});
});
observerSelect.observe(document.body, {
childList: true,
subtree: true
});
} else {
// IE9-IE10优化处理
// 借助定时器先观察,再统一处理
var arrMutationsList = [];
var timerRenderList = null;
var funMutationObserver = function (target, action, event) {
// 此时不检测DOM变化
if (funAutoInitAndWatching.flag === false || window.watching === false || target.nodeType != 1) {
return;
}
clearTimeout(timerRenderList);
if (target.tagName == 'SELECT') {
arrMutationsList.push({
action: action,
node: target
});
} else if (target.tagName == 'OPTION' && (arrMutationsList.length == 0 || arrMutationsList[arrMutationsList.length - 1].node.contains(target) == false)) {
arrMutationsList.push({
action: 'refresh',
node: event.relatedNode || target
});
}
// 定时器处理
timerRenderList = setTimeout(function () {
funAutoInitAndWatching.flag = false;
arrMutationsList.forEach(function (objList) {
// 插入节点
funSyncRefresh(objList.node, objList.action);
});
funAutoInitAndWatching.flag = true;
arrMutationsList = [];
}, 16);
};
document.body.addEventListener('DOMNodeInserted', function (event) {
funMutationObserver(event.target, 'refresh', event);
});
document.body.addEventListener('DOMNodeRemoved', function (event) {
// relatedNode
funMutationObserver(event.target, 'remove', event);
});
}
};
if (document.readyState != 'loading') {
funAutoInitAndWatching();
} else {
window.addEventListener('DOMContentLoaded', funAutoInitAndWatching);
}
return Select;
}));