UNPKG

lu2

Version:

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

426 lines (362 loc) 12.6 kB
/** * @Tips.js * @author zhangxinxu * @version * @Created: 15-06-25 * @edit: 17-06-19 * @edit: 20-06-09 edit by y2x * @edit: 20-11-03 by zxx */ import './Follow.js'; class Tips extends HTMLElement { static get observedAttributes () { return ['title', 'reverse', 'for', 'eventType', 'align']; } constructor (trigger, content, options) { super(); // trigger可以是ID选择器 if (typeof trigger == 'string' && /^#?\w+(?:[-_]\w+)*$/.test(trigger)) { trigger = document.getElementById(trigger.replace('#', '')); } if (trigger && trigger.tips) { trigger.tips(content, options); return trigger['ui-tips']; } this.target = null; } get title () { let strTitle = this.getAttribute('title'); if (strTitle) { this.setAttribute('data-title', strTitle); // 移除浏览器默认的title,防止交互冲突 this.removeAttribute('title'); } else { strTitle = this.getAttribute('data-title') || ''; } return strTitle; } set title (value) { this.setAttribute('data-title', value); // 屏幕阅读无障碍访问支持 this.setAttribute('aria-label', value); } get reverse () { return this.getAttribute('reverse') !== null || this.classList.contains('reverse'); } set reverse (value) { if (value) { this.setAttribute('reverse', ''); } else { this.removeAttribute('reverse'); } } get htmlFor () { return this.getAttribute('for'); } set htmlFor (v) { this.setAttribute('for', v); } get align () { return this.getAttribute('align') || 'auto'; } set align (v) { this.setAttribute('align', v); } get eventType () { return this.getAttribute('eventtype') || 'hover'; } set eventType (v) { this.setAttribute('eventtype', v); } get trigger () { const htmlFor = this.htmlFor; let eleTrigger; if (htmlFor) { eleTrigger = document.getElementById(htmlFor); } return eleTrigger || this; } create () { const eleTrigger = this.trigger; const strContent = this.title; // eleTips元素不存在,则重新创建 const eleTips = document.createElement('div'); eleTips.classList.add('ui-tips-x'); if (eleTrigger.id) { eleTips.classList.add('ui-tips-' + eleTrigger.id.replace(/[A-Z]/g, (matches) => `-${matches.toLowerCase()}`).replace(/^-+|-+$/g, '')); } eleTips.innerHTML = strContent; // 屏幕阅读无障碍访问描述 if (!eleTrigger.getAttribute('aria-label')) { // 创建随机id, aria需要 const strRandomId = 'lulu_' + (Math.random() + '').replace('0.', ''); eleTrigger.setAttribute('aria-labelledby', strRandomId); } // append到页面中 document.body.appendChild(eleTips); this.target = eleTips; // DOM this.dispatchEvent(new CustomEvent('DOMContentLoaded')); if (eleTrigger != this) { eleTrigger.dispatchEvent(new CustomEvent('DOMContentLoaded')); } } show () { let eleTrigger = this.trigger; let strContent = this.title; if (this.target) { // 改变显示内容 this.target.innerHTML = strContent; } else { this.create(); } // 显示 this.target.style.display = 'block'; // 定位 let strPosition = '5-7'; // 定位只有5-7, 7-5, 6-8, 8-6这几种 // 其中5-7是默认,提示在上方 // 7-5是由'reverse'类名或参数决定的 let strAlign = this.align; const isReverse = this.reverse; if (strAlign === 'auto') { strAlign = eleTrigger.dataset.align || eleTrigger.dataset.position || 'center'; } // 关键字参数与位置 if (strAlign === 'center') { strPosition = !isReverse ? '5-7' : '7-5'; } else if (strAlign === 'left') { strPosition = !isReverse ? '1-4' : '4-1'; } else if (strAlign === 'right') { strPosition = !isReverse ? '2-3' : '3-2'; } else if (/^\d-\d$/.test(strAlign)) { strPosition = strAlign; } eleTrigger.follow(this.target, { // trigger-target position: strPosition, // 边界溢出不自动修正 edgeAdjust: false }); // 同时以CSS变量的形式设置 trigger 的宽度和高度 this.target.style.setProperty('--ui-width', eleTrigger.offsetWidth); this.target.style.setProperty('--ui-height', eleTrigger.offsetHeight); if (eleTrigger.getAttribute('is-tips') == 'nowrap') { this.target.style.whiteSpace = 'nowrap'; } else { this.target.style.whiteSpace = ''; } // 显示的回调 eleTrigger.dispatchEvent(new CustomEvent('show', { detail: { type: 'ui-tips' } })); } hide () { if (!this.target) { return; } this.target.style.display = 'none'; this.trigger.dispatchEvent(new CustomEvent('hide', { detail: { type: 'ui-tips' } })); } events () { let eleTrigger = this.trigger; // hover显示延迟时间 const numDelay = 100; // 设置定时器对象 this.timerTips = null; this.handleMouseEnter = () => { this.timerTips = setTimeout(() => { this.show(); }, numDelay); }; this.handleMouseLeave = () => { clearTimeout(this.timerTips); // 隐藏提示 this.hide(); }; this.handleFocus = () => { if (window.isKeyEvent) { this.show(); } }; this.handleMouseUp = (event) => { const eleTarget = event.target; if (!eleTrigger.contains(eleTarget) && !this.target.contains(eleTarget)) { this.hide(); } }; // 事件走起 if (this.eventType === 'hover') { // 鼠标进入 eleTrigger.addEventListener('mouseenter', this.handleMouseEnter); // 鼠标离开 eleTrigger.addEventListener('mouseleave', this.handleMouseLeave); // 支持focus的显示与隐藏 // 但是需要是键盘访问触发的focus eleTrigger.addEventListener('focus', this.handleFocus); // 元素失焦 eleTrigger.addEventListener('blur', this.hide); } else if (this.eventType === 'click') { eleTrigger.addEventListener('click', this.show); // 关闭 document.addEventListener('mouseup', this.handleMouseUp); } else { // 其他事件类型直接显示 this.show(); } } connectedCallback () { let eleTrigger = this.trigger; // format title eleTrigger.originTitle = this.title; if (this.isConnectedCallback) { return; } // 更语义 // 非focusable元素使其focusable if (!/^a|input|button|area$/i.test(eleTrigger.tagName)) { eleTrigger.setAttribute('tabindex', '0'); // 更语义 eleTrigger.setAttribute('role', 'tooltip'); } this.events(); // 全局事件 this.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-tips' } })); if (eleTrigger != this && eleTrigger.hasAttribute('is-tips')) { eleTrigger.dispatchEvent(new CustomEvent('connected', { detail: { type: 'ui-tips' } })); // 设置定义完毕标志量 eleTrigger.setAttribute('defined', ''); } this.isConnectedCallback = true; } } if (!customElements.get('ui-tips')) { customElements.define('ui-tips', Tips); } window.Tips = Tips; /** * 给任意 dom 注入 tips 方法 * @param options {eventType, align} */ HTMLElement.prototype.tips = function (content, options = {}) { // 如果是CSS驱动的tips提示效果 if (this.getAttribute('is-tips') === 'css' || this.classList.contains('ui-tips')) { if (this.title) { this.setAttribute('data-title', this.title); this.setAttribute('aria-label', this.title); this.removeAttribute('title'); } return; } if (typeof content != 'string') { options = content || {}; } const isReverse = this.hasAttribute('reverse') || this.classList.contains('reverse'); // 只调用一次 if (this['ui-tips']) { this['ui-tips'].toggleAttribute('reverse', isReverse); // 显示与否的判断 if (typeof options.eventType != 'undefined' && options.eventType != 'hover' && options.eventType != 'click') { this['ui-tips'].show(); } return; } let eleTips = document.createElement('ui-tips'); if (typeof content == 'string') { eleTips.title = content; } else { eleTips.title = this.getAttribute('title') || options.content || ''; } // 是否反向的处理 eleTips.toggleAttribute('reverse', isReverse); // 移除原始的标题 this.removeAttribute('title'); // custom trigger if (!this.id) { this.id = 'lulu_' + (Math.random() + '').replace('0.', ''); } eleTips.htmlFor = this.id; if (options.eventType) { eleTips.eventType = options.eventType; } if (options.align) { eleTips.align = options.align; } this['ui-tips'] = eleTips; eleTips.addEventListener('connected', function () { this.remove(); }); document.body.appendChild(eleTips); }; (function () { // 处理所有非 <ui-tips /> 的情况: .ui-tips, [is-tips="css"], [is-tips] let funTipsInitAndWatching = function () { const strSelector = '.ui-tips, [is-tips]'; document.querySelectorAll(strSelector).forEach((item) => { if (item.tips) { item.tips(); } }); var observerTips = new MutationObserver(function (mutationsList) { mutationsList.forEach(function (mutation) { var nodeAdded = mutation.addedNodes; var nodeRemoved = mutation.removedNodes; if (nodeAdded.length) { nodeAdded.forEach(function (eleAdd) { if (!eleAdd.matches) { return; } if (eleAdd.matches(strSelector)) { eleAdd.tips(); } else { eleAdd.querySelectorAll(strSelector).forEach(item => { item.tips(); }); } }); } if (nodeRemoved.length) { nodeRemoved.forEach(function (eleRemove) { if (!eleRemove.matches) { return; } // 删除对应的<ui-tips>元素,如果有 if (eleRemove['ui-tips'] && eleRemove['ui-tips'].target) { eleRemove['ui-tips'].target.remove(); } else { eleRemove.querySelectorAll(strSelector).forEach(function (item) { if (item['ui-tips'] && item['ui-tips'].target) { item['ui-tips'].target.remove(); } }); } }); } }); }); observerTips.observe(document.body, { childList: true, subtree: true }); }; if (document.readyState != 'loading') { funTipsInitAndWatching(); } else { window.addEventListener('DOMContentLoaded', funTipsInitAndWatching); } })(); export default Tips;