lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
617 lines (519 loc) • 19.1 kB
JavaScript
/**
* @Drop.js
* @author zhangxinxu
* @version
* @created 15-06-30
* @edited 20-07-08 edit by wanglei
* @edited 22-06-16 edit by wanglei
*/
/**/import '../Float/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 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);
});
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 'click': {
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';
eleTarget.style.outline = 'none';
// 焦点转移
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;
}
/**
* <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')) {
setTimeout(() => {
if (this.list) {
this.list(eleTarget);
}
}, 1);
} 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);
}
return this;
};
/**
* 初始化所有包含is-drop属性的节点
* 支持事件冒泡,当点击的元素或者祖先元素,设置了is-drop属性,则把is-drop的属性值作为下拉元素的id,并根据这个id值获取并显示下拉元素。
*/
const initAllIsDropAttrAction = (ele) => {
const eleDrops = ele || document.querySelectorAll('[is-drop]');
eleDrops.forEach(eleTrigger => {
let eleTargetId = eleTrigger.getAttribute('is-drop');
if (eleTargetId && !eleTrigger.dataset.target) {
eleTrigger.dataset.target = eleTargetId;
}
// 基于data-target获取元素
eleTargetId = eleTrigger.dataset.target;
let eleTarget = eleTargetId && document.getElementById(eleTargetId);
if (eleTarget) {
eleTrigger['ui-drop'] = new Drop(eleTrigger, eleTarget);
}
});
};
/**
* 初始化并监听页面包含is-drop属性的DOM节点变化
*/
const autoInitAndWatchingIsDropAttr = () => {
// 先实例化已有is-drop属性的DOM节点,再监听后续的节点变化
initAllIsDropAttrAction();
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
mutation.addedNodes && mutation.addedNodes.forEach(eleAdd => {
if (!eleAdd.tagName) {
return;
}
if (eleAdd.hasAttribute('is-drop')) {
initAllIsDropAttrAction([eleAdd]);
} else {
initAllIsDropAttrAction(eleAdd.querySelectorAll('[is-drop]'));
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
if (document.readyState != 'loading') {
autoInitAndWatchingIsDropAttr();
} else {
window.addEventListener('DOMContentLoaded', autoInitAndWatchingIsDropAttr);
}
/**/export default Drop;