lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,526 lines (1,295 loc) • 56.3 kB
JavaScript
/**
* @Drop.js
* @author zhangxinxu
* @version
* @created 15-06-30
* @edited 20-07-08 edit by wanglei
*/
import './Follow.js';
class Drop extends HTMLElement {
static get observedAttributes () {
return ['open', 'target'];
}
static get defaults () {
return {
eventtype: 'click',
position: '7-5'
};
}
/**
* @param {Object} trigger 触发元素
* @param {Object} target 显示的浮动定位元素
* @param {Object} options 可选参数
*/
constructor (eleTrigger, eleTarget, options) {
super();
// 参数的处理
options = options || {};
this.params = this.params || {};
// 观察参数变化
this.params = new Proxy(this.params, {
get: (params, prop) => {
if (!prop) {
return;
}
prop = prop.toLowerCase();
let value = params[prop];
let eleTrigger = this.element.trigger;
if (typeof value == 'undefined') {
value = eleTrigger.getAttribute(prop) || eleTrigger.dataset[prop];
if (prop == 'width') {
if (eleTrigger !== this) {
value = eleTrigger.dataset[prop];
} else {
value = eleTrigger.getAttribute(prop);
}
}
// 部分参数如果没有,则使用默认的参数值
if (typeof value == 'undefined' && Drop.defaults[prop]) {
value = Drop.defaults[prop];
}
}
return value;
},
set: (params, prop, value) => {
params[prop.toLowerCase()] = value;
return true;
}
});
let objElement = this.element || {};
// trigger和target元素的设置与获取
this.element = new Proxy(objElement, {
get: (target, prop) => {
if (prop == 'target') {
let strIdTarget = this.getAttribute('target');
let eleTarget = target[prop];
if (!eleTarget && strIdTarget) {
eleTarget = document.getElementById(strIdTarget);
}
return eleTarget;
}
if (prop == 'trigger') {
return target[prop] || this;
}
return target[prop];
},
set: (target, prop, value) => {
if (typeof value == 'string') {
value = document.getElementById(value) || document.querySelector(value);
}
// 只有当value是节点元素时候才赋值
if (value && typeof value.nodeType != 'number') {
return false;
}
// 元素赋值
target[prop] = value;
// target元素设置时候同时需要赋值
if (prop == 'target' && value) {
let eleTarget = value;
let strId = eleTarget.id;
if (!strId) {
strId = ('lulu_' + Math.random()).replace('0.', '');
eleTarget.id = strId;
}
let eleTrigger = this.element.trigger;
// 如果用户直接使用this.element.target赋值,则需要同步target属性值
if (eleTrigger == this) {
// 此if判断可以避免死循环
if (this.target != strId) {
this.target = strId;
}
} else if (eleTrigger) {
let strAttrTarget = eleTrigger.dataset.target;
if (strAttrTarget && document.querySelector('datalist[id="' + strAttrTarget + '"]')) {
// 如果匹配的是<datalist>元素
eleTrigger.setAttribute('data-target2', strId);
} else {
eleTrigger.setAttribute('data-target', strId);
}
}
}
return true;
}
});
// 开始参数设置
// eleTrigger, eleTarget, options均可缺省
[...arguments].forEach(function (argument) {
if (typeof argument == 'object' && argument && !argument.tagName) {
options = argument;
}
});
if (eleTrigger) {
this.element.trigger = eleTrigger;
}
// 此时的eleTrigger一定是元素,之前可能是选择器
eleTrigger = this.element.trigger;
// target的处理
if (eleTrigger) {
// target元素
if (eleTarget && eleTarget !== options) {
this.element.target = eleTarget;
} else if (!eleTarget && eleTrigger.dataset && eleTrigger.dataset.target) {
this.element.target = eleTrigger.dataset.target;
}
}
// 参数设置
this.setParams(options);
// 如果默认open为true,则显示
if (this.open) {
if (document.readyState != 'loading') {
this.show();
} else {
window.addEventListener('DOMContentLoaded', () => {
this.show();
});
}
}
if (eleTrigger !== this) {
// 隐藏<ui-drop>元素细节
this.addEventListener('connected', function () {
this.remove();
});
eleTrigger['ui-drop'] = this;
document.body.append(this);
}
}
get htmlFor () {
return this.getAttribute('for');
}
set htmlFor (v) {
// 设置属性
this.setAttribute('for', v);
}
// 目标元素的设置与获取
get target () {
return this.getAttribute('target');
}
set target (value) {
this.setAttribute('target', value);
}
get eventType () {
return this.getAttribute('eventtype') || 'click';
}
set eventType (value) {
this.setAttribute('eventtype', value);
}
get open () {
return this.hasAttribute('open');
}
set open (value) {
this.toggleAttribute('open', value);
}
// 设置参数方法
setParams (options) {
options = options || {};
// 显示与隐藏的回调
let funCallShow = options.onShow;
let funCallHide = options.onHide;
if (typeof funCallShow == 'function') {
this.addEventListener('show', function (event) {
funCallShow.call(this, event);
// 自定义的显示方法需要重定位
this.position();
});
delete options.onShow;
}
if (typeof funCallHide == 'function') {
this.addEventListener('hide', function (event) {
funCallHide.call(this, event);
});
delete options.onHide;
}
// 参数合并
Object.assign(this.params, options || {});
}
/**
* 下拉定位的事件处理
* @return {[type]} [description]
*/
events (isIgnoreTarget) {
// 元素
let eleTrigger = this.element.trigger;
let eleTarget = this.element.target;
// 如果没有target 并且不是无视没有target,返回
if (!eleTarget && !isIgnoreTarget) {
return;
}
if (eleTarget && eleTarget.matches('datalist')) {
return;
}
// 参数
const objParams = this.params;
// 获取匹配选择器的eleTrigger子元素
const funGetClosestChild = (element) => {
if (!objParams.selector) {
return null;
}
const eleClosestSelector = element.closest(objParams.selector);
if (eleTrigger.contains(eleClosestSelector) == false) {
return null;
}
return eleClosestSelector;
};
// 根据不同事件类型进行逻辑处理
switch (objParams.eventType) {
// 默认值,直接显示
case 'null': {
break;
}
case 'hover': case 'mouseover': case 'mouseenter': {
// 如果不是无视target
if (!eleTarget) {
setTimeout(() => {
// vue, react等框架渲染时候,target可能会滞后
// 所以加个定时器处理
if (this.element.target) {
this.events();
}
}, 1);
// 一定要返回,否则下面的会报错
// click事件可以不返回,因为target不存在也可以触发
return;
}
// hover处理需要增加延时
eleTarget.timerHover = null;
// 同时,从trigger移动到target也需要延时,
// 因为两者可能有间隙,不能单纯移出就隐藏
eleTarget.timerHold = null;
// 此if逻辑用来避免事件重复绑定
// 因为Panel或List方法会再次执行一遍events()方法
if (!eleTrigger.isBindDropEvents) {
// 事件走起
eleTrigger.addEventListener('mouseover', event => {
// 不是在元素自身移动
if (event.relatedTarget !== event.target) {
// 委托查询
const eleClosestSelector = funGetClosestChild(event.target);
// 如果走委托
if (eleClosestSelector) {
// 改变trigger元素
this.element.trigger = eleClosestSelector;
}
// 显示定时器
if (!objParams.selector || eleClosestSelector) {
// 显示的定时器
eleTarget.timerHover = setTimeout(() => {
this.show();
}, 150);
// 去除隐藏的定时器
clearTimeout(eleTarget.timerHold);
}
}
});
eleTrigger.addEventListener('mouseout', (event) => {
// 这个 if 判断主要是兼容设置了 options.selector 参数的场景
// 避免容器元素鼠标移出的时候才隐藏,因为是容器内委托的元素移除隐藏
if (this.element.trigger == event.target || this.element.trigger.contains(event.target)) {
// 清除显示的定时器
clearTimeout(eleTarget.timerHover);
// 隐藏的定时器
eleTarget.timerHold = setTimeout(() => {
this.hide();
}, 200);
}
});
if (eleTarget && !eleTarget.isBindDropHover) {
eleTarget.addEventListener('mouseenter', () => {
// 去除隐藏的定时器
clearTimeout(eleTarget.timerHold);
});
eleTarget.addEventListener('mouseleave', () => {
// 隐藏
eleTarget.timerHold = setTimeout(() => {
// 需要触发元素也是hover类型
let eleRelatedTrigger = eleTarget.element.trigger;
// eleRelatedTrigger是<ui-drop>元素
if (eleRelatedTrigger && eleRelatedTrigger.eventType == 'hover') {
eleRelatedTrigger.hide();
}
}, 100);
});
eleTarget.isBindDropHover = true;
}
// 键盘支持,原本使用focus事件,但并不利于键盘交互
eleTrigger.addEventListener('click', event => {
// window.isKeyEvent表示是否键盘触发,来自Keyboard.js
if (!window.isKeyEvent) {
return;
}
event.preventDefault();
const eleClosestSelector = funGetClosestChild(event.target);
// 如果走委托
if (eleClosestSelector) {
// 改变trigger元素
this.element.trigger = eleClosestSelector;
}
// 显示定时器
if (!objParams.selector || eleClosestSelector) {
// 点击即显示
if (!this.open) {
this.show();
} else {
this.hide();
}
}
});
// 统一设置事件类型为hover
if (this.eventType != 'hover') {
this.eventType = 'hover';
}
}
break;
}
// 点击或者右键
case 'click': case 'contextmenu': {
if (!eleTrigger.isBindDropEvents || eleTrigger.isBindDropEvents !== objParams.eventType) {
eleTrigger.addEventListener(objParams.eventType, event => {
event.preventDefault();
// aria支持
// 获得委托的选择器匹配元素
const eleClosestSelector = funGetClosestChild(event.target);
if (eleClosestSelector) {
// 改变trigger元素
this.element.trigger = eleClosestSelector;
}
// 点击即显示
if (!objParams.selector || eleClosestSelector) {
// 连续右键点击保持显示,非显隐切换
if (objParams.eventType == 'contextmenu') {
objParams.position = [event.pageX, event.pageY];
this.show();
return;
}
if (!this.open) {
this.show();
} else {
this.hide();
}
}
});
}
break;
}
default: {
break;
}
}
// 点击页面空白区域隐藏
if (objParams.eventType != 'null' && !eleTrigger.isBindDocMouseUp) {
document.addEventListener('mouseup', event => {
let eleClicked = event && event.target;
if (!eleClicked || !this.open) {
return;
}
// 因为trigger和target可能动态变化
// 因此这里再次获取一遍
let eleTrigger = this.element.trigger;
let eleTarget = this.element.target;
if (eleTrigger.contains(eleClicked) == false && (!eleTarget || eleTarget.contains(eleClicked) == false)) {
this.hide();
}
});
eleTrigger.isBindDocMouseUp = true;
}
eleTrigger.isBindDropEvents = objParams.eventType || true;
// 窗体尺寸改变生活的重定位
window.addEventListener('resize', () => {
this.position();
});
this.dispatchEvent(new CustomEvent('DOMContentLoaded'));
if (eleTrigger != this) {
eleTrigger.dispatchEvent(new CustomEvent('DOMContentLoaded'));
}
}
/**
* 下拉定位处理
* @return {Object} 返回当前自定义元素
*/
position () {
let eleTrigger = this.element.trigger;
let eleTarget = this.element.target;
// 下拉必须是显示状态才执行定位处理
if (this.open && eleTarget && window.getComputedStyle(eleTrigger).display != 'none') {
eleTrigger.follow(eleTarget, {
offsets: this.params.offsets,
position: this.params.position,
edgeAdjust: this.params.edgeAdjust
});
}
return this;
}
/**
* 下拉的显示处理
* @return {Object} 返回当前自定义元素
*/
show () {
// target需要在页面中
let eleTrigger = this.element.trigger;
let eleTarget = this.element.target;
// 如果target在内存中,append到页面上
if (eleTarget && document.body.contains(eleTarget) == false) {
document.body.appendChild(eleTarget);
}
if (eleTarget) {
// 进行定位
eleTarget.style.position = 'absolute';
eleTarget.style.display = 'inline';
// 键盘ESC隐藏支持
eleTarget.classList.add('ESC');
eleTarget.tabIndex = -1;
// 焦点转移到浮层元素上
eleTarget.focus({
preventScroll: true
});
// 标记target此时对应的trigger元素
eleTarget.element = eleTarget.element || {};
eleTarget.element.trigger = this;
}
// aria
eleTrigger.setAttribute('aria-expanded', 'true');
// 改变显示标志量
if (!this.open) {
this.open = true;
}
// 定位
this.position();
// 触发自定义事件 - show
this.dispatchEvent(new CustomEvent('show'));
// 如果trigger元素非<ui-drop>元素,则也触发一下,同时传递show事件来源
// 因为一个普通的HTML元素可能会LuLu ui不同的组件触发'show'事件
if (eleTrigger != this) {
eleTrigger.dispatchEvent(new CustomEvent('show', {
detail: {
type: 'ui-drop'
}
}));
}
return this;
}
/**
* 下拉的隐藏处理
* @return {Object} 返回当前自定义元素
*/
hide () {
let eleTrigger = this.element.trigger;
let eleTarget = this.element.target;
// 隐藏下拉面板
if (eleTarget) {
eleTarget.style.display = 'none';
eleTarget.classList.remove('ESC');
// 取消target元素上的trigger关联
// 因为一个target元素可以关联多个trigger
if (eleTarget.element) {
delete eleTarget.element.trigger;
}
}
// aria
eleTrigger.setAttribute('aria-expanded', 'false');
if (window.isKeyEvent) {
eleTrigger.focus();
}
// 更改显示标志量
if (this.open) {
this.open = false;
}
// 触发自定义事件 - hide
this.dispatchEvent(new CustomEvent('hide'));
if (eleTrigger != this) {
eleTrigger.dispatchEvent(new CustomEvent('hide', {
detail: {
type: 'ui-drop'
}
}));
}
return this;
}
/**
* drop 拓展list
* @date 2019-11-01
* @returns {object} 返回当前自定义元素
* 兼容以下几种语法
* new Drop().list(eleTrigger, data);
* new Drop().list(eleTrigger, data, options);
* new Drop(eleTrigger).list(data, options);
* new Drop(eleTrigger, options).list(data);
*/
list (eleTrigger, data, options) {
// datalist 元素的 option 转为列表数据
const funGetDataByOption = function (option) {
let obj = {
id: option.id || option.value,
value: option.innerHTML || option.value,
className: option.className,
disabled: option.disabled,
label: option.label,
accessKey: option.accessKey
};
// disabled处理
let eleOptgroup = option.closest('optgroup');
if (eleOptgroup && eleOptgroup.disabled) {
obj.disabled = true;
}
// href 属性处理
if (option.hasAttribute('href')) {
obj.href = option.getAttribute('href');
}
return obj;
};
// 基于类型进行参数判断
[...arguments].forEach(argument => {
const strTypeArgument = typeof argument;
if (strTypeArgument === 'string') {
eleTrigger = document.getElementById(argument) || document.querySelector(argument);
} else if (strTypeArgument === 'function') {
data = argument;
} else if (strTypeArgument === 'object') {
if (typeof argument.nodeType === 'number') {
// 支持从原生的列表元素中获取数据信息
if (argument.matches('datalist')) {
data = function () {
// 所有分组元素
let eleOptgroups = argument.querySelectorAll('optgroup');
// 如果有任意一个分组元素设置了label,那么就是标题分组
// 如果只是optgroup标签包括,那么使用分隔线分隔
let isSubTitle = !!argument.querySelector('optgroup[label]');
// 如果有分组
if (eleOptgroups.length) {
let arrData = [];
eleOptgroups.forEach(optgroup => {
if (isSubTitle) {
arrData.push({
id: '-1',
value: optgroup.label,
disabled: optgroup.disabled,
className: optgroup.className,
heading: true
});
} else {
// 分隔线
arrData.push({});
}
optgroup.querySelectorAll('option').forEach(option => {
arrData.push(funGetDataByOption(option));
});
});
return arrData;
}
return [...argument.querySelectorAll('option')].map((option, index) => {
let objOption = funGetDataByOption(option);
if (!objOption.value) {
return {};
}
if (!objOption.id) {
objOption.id = index;
}
return objOption;
});
};
if (eleTrigger == argument) {
eleTrigger = null;
}
} else {
eleTrigger = argument;
}
} else if (argument.map) {
data = argument;
} else {
options = argument;
}
}
});
if (eleTrigger && typeof eleTrigger.nodeType !== 'number') {
eleTrigger = null;
}
eleTrigger = eleTrigger || this.element.trigger;
// 触发元素和数据是必须项
if (!eleTrigger) {
return this;
}
if (!data) {
data = [];
}
const defaults = {
// 触发元素显示的事件,‘null’直接显示;‘hover’是hover方法;‘click’是点击显示;其他为手动显示与隐藏。
eventType: 'click',
offsets: {
x: 0,
y: 0
},
position: '4-1',
selector: '',
width: '',
onShow: () => {},
onHide: () => {},
// this为当前点击的列表元素,支持两个参数,第一个参数为列表元素对应的数据(纯对象),第二个是当前实例对象
onSelect: () => {}
};
// 参数处理
const objParams = {};
options = options || {};
Object.keys(defaults).forEach(prop => {
objParams[prop] = options[prop] || this.params[prop] || defaults[prop];
});
// 一些常量
const SELECTED = 'selected';
const DISABLED = 'disabled';
// ui类名
// 类名变量
// 样式类名统一处理
const CL = {
add: function () {
return ['ui-droplist'].concat([].slice.call(arguments)).join('-');
},
toString: () => {
return 'ui-droplist';
}
};
// trigger元素赋值的方法
let strMethod = 'innerHTML';
if (eleTrigger.matches('input')) {
strMethod = 'value';
}
// target元素创建
let eleTarget = document.createElement('div');
eleTarget.setAttribute('role', 'listbox');
eleTarget.setAttribute('tabindex', '-1');
// 宽度处理
if (/^\d+$/.test(objParams.width)) {
eleTarget.style.width = objParams.width + 'px';
} else {
eleTarget.style.width = objParams.width;
}
eleTarget.className = CL.add('x');
// 存储原始参数值
this.data = data;
// 列表渲染需要的数组项
let arrListData = data;
// 索引值内容的匹配
let funGetMatchIndex = (arr) => {
// 初始的匹配和默认索引值获取(如果没有设置selected选项)
// 匹配的索引值
let strMatchIndex = '-1';
// 是否默认包含选中项
let isSomeItemSelected = false;
// 遍历数据
// 目的是获得匹配索引,和改变默认值(如果有设置selected选项)
// 由于数据可能无限嵌套,因此走递归
let funRecursion = (arrData, arrIndex) => {
if (!arrData || !arrData.length) {
return;
}
arrData.forEach((objData, numIndex) => {
// 此时数组项的索引深度
const arrCurrentIndex = arrIndex.concat(numIndex);
// 多级数据结构
if (objData && objData.data) {
funRecursion(objData.data, arrCurrentIndex);
return;
}
if (objData && objData[SELECTED] && !objData[DISABLED] && objData.value) {
eleTrigger[strMethod] = objData.value;
// 找到设置了selected的选项,并记住索引
strMatchIndex = arrCurrentIndex.join('-');
}
// 修改全局判断,这样,eleTrigger无需
// 再根据本身内容确定默认的选中项了
if (objData && objData[SELECTED]) {
isSomeItemSelected = true;
}
});
};
funRecursion(arr, []);
// 此时trigger元素内部的内容
const strTrigger = (eleTrigger[strMethod] || '').trim();
// 根据eleTrigger的内容信息确定哪个数据是selected
// 遍历数据
if (isSomeItemSelected == false && strTrigger) {
funRecursion = (arrData, arrIndex) => {
if (!arrData || !arrData.length) {
return;
}
arrData.forEach((objData, numIndex) => {
// 此时数组项的索引深度
const arrCurrentIndex = arrIndex.concat(numIndex);
// 多级数据结构
if (objData && objData.data) {
funRecursion(objData.data, arrCurrentIndex);
return;
}
// 如果有匹配,设置为选中
if (typeof objData.value === 'string' && objData.value.trim() == strTrigger) {
strMatchIndex = arrCurrentIndex.join('-');
// 设置为选中
objData[SELECTED] = true;
}
});
};
funRecursion(arr, []);
}
return strMatchIndex;
};
let strMatchIndex = -1;
if (typeof data !== 'function' && data.length && data.map) {
strMatchIndex = funGetMatchIndex(arrListData);
}
// 列表绘制渲染方法
// 每次show的时候都执行刷新
const funRender = (eleTarget, arrListData) => {
if (typeof arrListData === 'function') {
arrListData = arrListData();
// 重新获取索引值,只有data是function类型的时候才执行
strMatchIndex = funGetMatchIndex(arrListData);
}
// 没有数据时候的处理
if (!arrListData || !arrListData.length) {
arrListData = [{
value: '没有数据',
disabled: true
}];
} else {
arrListData = arrListData.map(arrData => {
if (typeof arrData == 'string' && arrData !== '-') {
return {
value: arrData
};
}
return arrData;
});
}
// 是否包含选中项
let isSomeItemSelected = arrListData.some(objData => {
return objData && objData[SELECTED];
});
// 列表数据更新
eleTarget.innerHTML = (() => {
let strHtml = '';
const funStep = (arrData, arrIndex) => {
let strHtmlStep = '';
arrData.forEach((objData, numIndex) => {
// 为空数据作为分隔线
if (objData == '-' || objData === null || JSON.stringify(objData) == '{}') {
strHtmlStep += '<hr class="' + CL.add('hr') + '">';
return;
}
// 此时数组项的索引深度
const arrCurrentIndex = arrIndex.concat(numIndex);
// 一些属性值
let strAttrHref = objData.href;
if (typeof strAttrHref != 'string') {
strAttrHref = 'javascript:';
} else if (!strAttrHref) {
strAttrHref = location.href.split('#')[0];
}
// target属性
let strAttrTarget = '';
if (objData.target) {
strAttrTarget = ' target="' + objData.target + '"';
}
// 是否包含子项
let strAttrSublist = '';
if (objData.data) {
strAttrSublist = ' data-sublist';
}
// label标记
let strAttrLabel = '';
if (objData.label) {
strAttrLabel = ' aria-label="' + objData.label + '"';
}
// accesskey快捷访问
let strAttrAccess = '';
if (objData.accessKey) {
strAttrAccess = ` accesskey="${objData.accessKey}"`;
}
// 如果数据不含选中项,使用存储的索引值进行匹配
if (isSomeItemSelected == false && strMatchIndex == arrCurrentIndex.join('-')) {
objData[SELECTED] = true;
}
// 类名
let strAttrClass = CL.add('li') + ' ' + objData.className;
if (objData[SELECTED]) {
strAttrClass = strAttrClass + ' ' + SELECTED;
}
strAttrClass = strAttrClass.trim();
// 如果是标题元素
if (objData.heading == true) {
if (objData.disabled) {
strAttrClass += ' disabled';
}
strHtmlStep += '<div class="' + strAttrClass + '"' + strAttrLabel + ' role="heading">' + objData.value + '</div>';
return;
}
// 禁用态和非禁用使用标签区分
// 如果想要支持多级,data-index值可以"1-2"这样
if (objData[DISABLED] != true) {
strHtmlStep += '<a href="' + strAttrHref + '"' + strAttrTarget + strAttrLabel + strAttrAccess + ' class="' + strAttrClass + '" data-index="' + arrCurrentIndex.join('-') + '" role="option" aria-selected="' + (objData[SELECTED] || 'false') + '" ' + strAttrSublist + '>' + objData.value + '</a>';
if (objData.data) {
strHtmlStep += '<div class="' + CL.add('xx') + '"><div class="' + CL.add('x') + '" role="listbox">' + funStep(objData.data, arrCurrentIndex) + '</div></div>';
}
} else {
strHtmlStep += '<span class="' + strAttrClass + '"' + strAttrLabel + strAttrAccess + '>' + objData.value + '</span>';
}
});
return strHtmlStep;
};
strHtml += funStep(arrListData, []);
return strHtml;
})();
// 存储在DOM对象上,给回调使用
eleTarget.listData = arrListData;
};
// 重新初始化 drop
this.setParams({
eventType: objParams.eventType,
offsets: objParams.offsets,
selector: objParams.selector,
position: objParams.position,
onShow: function () {
funRender.call(this, eleTarget, this.data);
objParams.onShow.apply(this, arguments);
},
onHide: objParams.onHide
});
this.element.trigger = eleTrigger;
this.element.target = eleTarget;
// 新的事件
this.events();
// 绑定事件
eleTarget.addEventListener('click', event => {
// IE可能是文件节点
if (event.target.nodeType != 1 || !event.target.closest) {
return;
}
// 目标点击元素
const eleClicked = event.target.closest('a');
// 未获取到元素返回
if (!eleClicked) {
return;
}
// 当前列表显示使用的数据
const arrListData = eleTarget.listData;
// 如果是多级嵌套列表,这里需要额外处理
const strIndex = eleClicked.getAttribute('data-index');
if (!strIndex) {
return;
}
// 根据点击的元素索引获取对应的数据对象
let objItemData = null;
strIndex.split('-').forEach(numIndex => {
if (objItemData === null) {
objItemData = arrListData[numIndex];
} else if (objItemData.data) {
objItemData = objItemData.data[numIndex];
} else {
objItemData = objItemData[numIndex];
}
});
// 如果这里返回,说明数据有问题
// 或者代码逻辑有bug
if (!objItemData) {
return;
}
// 点击包含下级数据的列表
if (typeof eleClicked.getAttribute('data-sublist') === 'string') {
eleClicked.classList.add(SELECTED);
// 此时显示的下级列表元素
const eleSubTarget = eleClicked.nextElementSibling.querySelector('.' + CL.add('x'));
if (!eleSubTarget) {
return;
}
// 偏移还原
// 否则会A/B重定位
eleSubTarget.style.transform = '';
eleSubTarget.classList.remove('reverse');
// 此时偏移
const objBounding = eleSubTarget.getBoundingClientRect();
// 水平方向是方向变化,这里使用document.documentElement宽度判断
// 因为window.innerWidth包括滚动条宽度,可能会导致水平滚动条出现
// 交给CSS控制
if (objBounding.right > document.documentElement.clientWidth) {
eleSubTarget.classList.add('reverse');
}
// 垂直方向偏移
let offsetTop = 0;
if (objBounding.bottom > window.innerHeight) {
offsetTop = window.innerHeight - objBounding.bottom;
}
eleSubTarget.style.transform = 'translateY(' + offsetTop + 'px)';
return;
}
// 改变选中索引
if (strIndex != strMatchIndex) {
let objLastItemData = null;
if (strMatchIndex != '-1') {
strMatchIndex.split('-').forEach(numIndex => {
if (objLastItemData === null) {
objLastItemData = arrListData[numIndex];
} else if (objLastItemData.data) {
objLastItemData = objLastItemData.data[numIndex];
} else {
objLastItemData = objLastItemData[numIndex];
}
});
if (objLastItemData) {
delete objLastItemData[SELECTED];
}
}
// 设置为true
objItemData[SELECTED] = true;
// 更新匹配索引
strMatchIndex = strIndex;
}
// 触发用户自定义选择事件
(this.params.onSelect || objParams.onSelect).call(this, objItemData, eleClicked);
// 触发自定义事件 - select
this.dispatchEvent(new CustomEvent('select', {
detail: {
data: objItemData,
target: eleClicked
}
}));
// 如果trigger元素非<ui-drop>元素,则也触发一下,同时传递select事件来源
// 如果是在Vue中,可以使用@select绑定选择事件
if (eleTrigger != this) {
eleTrigger.dispatchEvent(new CustomEvent('select', {
detail: {
type: 'ui-drop',
data: objItemData,
target: eleClicked
}
}));
}
// 不是鼠标右击事件,也不是委托模式更新
if (objParams.eventType != 'contextmenu' && objParams.selector == '' && !objItemData.href) {
eleTrigger[strMethod] = objItemData.value;
}
// 隐藏
this.hide();
});
// hover时候次级列表也显示
eleTarget.addEventListener('mouseover', event => {
// IE可能是文件节点
if (event.target.nodeType != 1 || !event.target.closest) {
return;
}
const eleHovered = event.target.closest('a');
if (!eleHovered) {
return;
}
const eleItemSublist = eleHovered.parentElement.querySelector('.' + SELECTED + '[data-sublist]');
if (eleItemSublist && eleItemSublist != eleHovered) {
eleItemSublist.classList.remove(SELECTED);
}
if (eleHovered.classList.contains(SELECTED) == false && typeof eleHovered.getAttribute('data-sublist') === 'string') {
eleHovered.click();
}
});
return this;
}
/**
* drop 拓展panel
* @date 2019-11-01
* 兼容以下两种语法
* new Drop().panel(eleTrigger, options);
* new Drop(eleTrigger).panel(options);
* new Drop(eleTrigger).panel(eleTarget);
* @returns {object} 返回当前自定义元素
*/
panel (eleTrigger, options) {
// 不同类型参数的处理
if (arguments.length === 2) {
eleTrigger = arguments[0];
options = arguments[1];
} else if (arguments.length === 1) {
options = arguments[0];
if (options.matches && options.matches('dialog')) {
let eleTarget = options;
// 按钮信息
let strButtons = eleTarget.dataset.buttons;
var arrButtons = [];
// 如果data-buttons为空字符串,或者是 null、false, 则不显示按钮
if (strButtons !== '' && strButtons !== 'null' && strButtons !== 'false') {
// 如果strButtons不包含{ 和 },数组直接split处理
strButtons = strButtons || '';
let arrParse = [];
if (!strButtons.includes('{') && !strButtons.includes('}')) {
arrParse = strButtons.split(',').map(item => {
return {
value: item.trim()
};
});
} else {
// 转为数组
try {
arrParse = (new Function('return [' + strButtons + ']'))();
} catch (e) {
console.error('按钮解析失败', e);
arrParse = [];
}
}
// 前后按钮对象
const objStart = arrParse[0] || {};
const objEnd = arrParse[1] || {};
// 处理按钮的事件
arrButtons = [{
...objStart,
events: () => {
eleTarget.dispatchEvent(new CustomEvent('ensure', {
detail: {
drop: this
}
}));
}
}, {
...objEnd,
events: () => {
eleTarget.dispatchEvent(new CustomEvent('cancel', {
detail: {
drop: this
}
}));
this.hide();
}
}];
}
options = {
content: eleTarget.innerHTML,
title: eleTarget.title,
buttons: arrButtons
};
// 如果设置了关闭按钮
if (eleTarget.dataset.closable == 'false') {
options.closable = false;
}
}
eleTrigger = null;
}
if (typeof eleTrigger === 'string') {
eleTrigger = document.querySelector(eleTrigger);
}
eleTrigger = eleTrigger || this.element.trigger;
if (!eleTrigger) {
return this;
}
options = options || {};
// 支持从 trigger 元素上获取部分参数
['width', 'eventType', 'selector', 'offsets', 'position', 'closable'].forEach(function (strKey) {
const strAttrKey = eleTrigger.getAttribute(strKey) || eleTrigger.dataset[strKey.toLowerCase()];
if (strAttrKey && typeof options[strKey] == 'undefined') {
options[strKey] = strAttrKey;
}
});
const defaults = {
title: '',
content: '',
buttons: [{}, {}],
width: 'auto',
eventType: 'click',
selector: '',
offsets: {
x: 0,
y: 0
},
position: '4-1',
onShow: () => { },
onHide: () => { }
};
// Drop 配置
const objParams = Object.assign({}, defaults, options);
// ui类名
// 类名变量
// 样式类名统一处理
const CL = {
add: function () {
return ['ui-dropanel'].concat([].slice.call(arguments)).join('-');
},
toString: () => {
return 'ui-dropanel';
}
};
// 面板容器
const elePanel = document.createElement('div');
elePanel.className = CL.add('x');
// 面板的宽度设置
// 如果纯数值,认为是px长度
if (/^\d+$/.test(objParams.width)) {
elePanel.style.width = objParams.width + 'px';
} else if (/^\d+\D+$/.test(objParams.width)) {
elePanel.style.width = objParams.width;
}
// 创建轻弹框面板的各个元素
// title
const eleTitle = document.createElement('h5');
eleTitle.className = CL.add('title');
eleTitle.innerHTML = objParams.title;
// close button
const eleClose = document.createElement('button');
eleClose.setAttribute('aria-label', '关闭');
eleClose.className = CL.add('close');
// content
const eleContent = document.createElement('content');
eleContent.className = CL.add('content');
eleContent.innerHTML = objParams.content;
// footer
const eleFooter = document.createElement('div');
eleFooter.className = CL.add('footer');
// 组装
elePanel.appendChild(eleTitle);
if (objParams.closable !== false && objParams.closable != 'false') {
elePanel.appendChild(eleClose);
}
elePanel.appendChild(eleContent);
elePanel.appendChild(eleFooter);
// 初始化
this.setParams({
eventType: objParams.eventType,
offsets: objParams.offsets,
// 实现点击或hover事件的委托实现
selector: objParams.selector,
position: objParams.position,
onShow: objParams.onShow,
onHide: objParams.onHide
});
// 重新添加element
Object.assign(this.element, {
trigger: eleTrigger,
target: elePanel,
panel: elePanel,
title: eleTitle,
close: eleClose,
content: eleContent,
footer: eleFooter
});
// 事件初始化
this.events();
// 绑定事件
// 关闭事件
eleClose.addEventListener('click', () => {
this.hide();
}, false);
// 按钮
objParams.buttons.forEach((objBtn, numIndex) => {
// 避免btn为null等值报错
objBtn = objBtn || {};
// 按钮的默认参数
let strType = objBtn.type || '';
if (!strType && numIndex == 0) {
strType = objParams.buttons.length > 1 ? 'danger' : 'primary';
}
let strValue = objBtn.value;
if (!strValue) {
strValue = ['确定', '取消'][numIndex];
}
// 如果没有指定事件,则认为是关闭行为
let objEvents = objBtn.events || {
click: () => {
this.hide();
}
};
// 如果没有指定事件类型,直接是函数
// 则认为是点击事件
if (typeof objEvents === 'function') {
objEvents = {
click: objEvents
};
}
let eleBtn = null;
// 普通按钮
if (objBtn['for']) {
eleBtn = document.createElement('label');
eleBtn.setAttribute('for', objBtn['for']);
eleBtn.setAttribute('role', 'button');
} else if (objBtn.form) {
eleBtn.setAttribute('form', objBtn.form);
eleBtn.type = 'submit';
} else {
eleBtn = document.createElement('button');
this.element['button' + numIndex] = eleBtn;
}
// 按钮的文案
eleBtn.innerHTML = strValue;
// 按钮类名
eleBtn.className = String(CL).replace('dropanel', 'button') + ' ' + CL.add('button') + ' ' + (objBtn.className || '');
// 按钮的类型
eleBtn.setAttribute('data-type', strType || 'normal');
this.element['button' + numIndex] = eleBtn;
for (let strEventType in objEvents) {
eleBtn.addEventListener(strEventType, event => {
event.drop = this;
objEvents[strEventType](event);
}, false);
}
this.element.footer.appendChild(eleBtn);
});
return this;
}
/**
* <ui-drop>元素进入页面中的时候
**/
connectedCallback () {
let eleTarget = this.element.target;
let eleTrigger = this.element.trigger;
// 默认的aria状态设置
if (eleTrigger.open) {
eleTrigger.setAttribute('aria-expanded', 'true');
} else {
eleTrigger.setAttribute('aria-expanded', 'false');
}
// 页面中的<ui-drop>元素如果没有target
// 强制绑定事件,这样<ui-drop>元素可以实现open切换效果
if (!eleTarget) {
this.events(eleTrigger === this);
} else if (eleTarget.matches('datalist')) {
this.list(eleTarget);
} else if (eleTarget.matches('dialog')) {
this.panel(eleTarget);
} else {
this.events();
}
// 无障碍访问设置
if (!this.querySelector('a, button') && !this.closest('a, button')) {
this.tabIndex = 0;
this.role = 'button';
}
// 全局事件
this.dispatchEvent(new CustomEvent('connected', {
detail: {
type: 'ui-drop'
}
}));
if (eleTrigger != this && eleTrigger.hasAttribute('is-drop')) {
eleTrigger.dispatchEvent(new CustomEvent('connected', {
detail: {
type: 'ui-drop'
}
}));
// 设置定义完毕标志量
eleTrigger.setAttribute('defined', '');
}
this.isConnectedCallback = true;
}
// open属性变化的时候
attributeChangedCallback (name, oldValue, newValue) {
if (name == 'target') {
let eleTarget = document.getElementById(newValue);
if (eleTarget) {
this.element.target = eleTarget;
}
} else if (name == 'open') {
let strAriaExpanded = this.element.trigger.getAttribute('aria-expanded');
if (this.open && strAriaExpanded == 'false') {
this.show();
} else if (!this.open && strAriaExpanded == 'true') {
this.hide();
}
}
}
}
window.Drop = Drop;
if (!customElements.get('ui-drop')) {
customElements.define('ui-drop', Drop);
}
// 给 HTML 元素扩展 drop 方法
HTMLElement.prototype.drop = function (eleTarget, options) {
if (!this.matches('ui-drop, [is-drop]') && !this['ui-drop']) {
this['ui-drop'] = new Drop(this, eleTarget, options);
}