Files
layui/dist/core/lay.js
2025-11-04 15:09:10 +08:00

1207 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { layui } from './layui.js';
/**
* lay
* 基础模块
*/
var document = window.document;
/**
* 元素查找
* @param {string | HTMLElement | JQuery} selector
*/
var lay = function (selector) {
return new Class(selector);
};
// 构造器
var Class = function (selector) {
var that = this;
var elem = typeof selector === 'object' ? function () {
// 仅适配简单元素对象
return layui.isArray(selector) ? selector : [selector];
}() : (this.selector = selector, document.querySelectorAll(selector || null));
lay.each(elem, function (index) {
that.push(elem[index]);
});
};
var fnToString = Function.prototype.toString;
var ObjectFunctionString = fnToString.call(Object);
var hasOwnProperty = Object.prototype.hasOwnProperty;
/*
lay 对象操作
*/
Class.fn = Class.prototype = [];
Class.fn.constructor = Class;
/**
* 将一个或多个对象合并到目标对象中
* 对象类型值始终进行「深拷贝」合并。若需浅拷贝合并,请使用 Object.assign()
* @param {*} target - 目标对象
* @param {...*} objectN - 一个或多个包含要应用的属性的源对象
* @param {Function} customizer - 可选的自定义合并函数
* @returns {*} 返回合并后的对象
* @example
*```js
* console.log(lay.extend({}, {a:1})); // expected: {a:1}
* console.log(lay.extend({a:1}, {a:3}, {a:5,b:5})); // expected: {a:5,b:5}
* // 多个相同源对象的不同合并方式
* const objN = [
* {
* a: [1, 3],
* b: {ba: 1}
* },
* {
* a: [5],
* b: {bb: 2}
* },
* {
* b: {ba: 3},
* c: 3
* }
* ];
* console.log(lay.extend({}, ...objN)); // expected: {a:[5,3],b:{ba:3,bb:2},c:3}
* // 使用 customizer 实现数组覆盖而非合并
* const obj1 = lay.extend({}, ...objN, function(objValue, srcValue) {
* if (Array.isArray(objValue) && Array.isArray(srcValue)) {
* return srcValue;
* }
* });
* console.log(obj1); // expected: {a:[5],b:{ba:3,bb:2},c:3}
* // 使用 customizer 实现特定字段跳过合并
* const obj2 = lay.extend({}, ...objN, function(objValue, srcValue, key, target, source) {
* if (key === 'b') {
* return objValue;
* }
* });
* console.log(obj2); // expected: {a:[5,3],b:{ba:1},c:3}
* ```
*/
lay.extend = function () {
var args = [].slice.call(arguments);
// 最后一个参数是否为 customizer
var customizer = typeof args[args.length - 1] === 'function' ? args.pop() : false;
// 深拷贝合并
return args.reduce(function (target, source) {
// 确保 target 始终是一个对象
if (typeof target !== 'object' || target === null) {
target = {};
}
for (var key in source) {
if (!hasOwnProperty.call(source, key)) continue; // 仅处理自有属性
var targetValue = target[key];
var sourceValue = source[key];
// 自定义合并逻辑(如数组覆盖、特定字段跳过等)
if (customizer) {
var customResult = customizer(targetValue, sourceValue, key, target, source);
if (customResult !== undefined) {
target[key] = customResult;
continue;
}
}
// 默认深拷贝逻辑
if (Array.isArray(sourceValue)) {
targetValue = Array.isArray(targetValue) ? targetValue : [];
} else if (lay.isPlainObject(sourceValue)) {
targetValue = lay.isPlainObject(targetValue) ? targetValue : {};
}
target[key] = lay.isPlainObject(sourceValue) || Array.isArray(sourceValue) ? lay.extend(targetValue, sourceValue, customizer) : sourceValue;
}
return target;
});
};
/**
* 判断是否为纯对象
* @param {*} obj - 要检查的对象
* @returns {boolean}
*/
lay.isPlainObject = function (obj) {
if (obj === null || typeof obj !== 'object' || Object.prototype.toString.call(obj) !== '[object Object]') {
return false;
}
var proto = Object.getPrototypeOf(obj);
// Object.create(null) 创建的对象
if (proto === null) {
return true;
}
// 判定具有原型且由全局 Object 构造函数创建的对象为纯对象(来自 jQuery 方案)
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString;
};
/**
* IE 版本
* @type {string | boolean} - 如果是 IE 返回版本字符串,否则返回 false
*/
lay.ie = function () {
var agent = navigator.userAgent.toLowerCase();
return !!window.ActiveXObject || 'ActiveXObject' in window ? (agent.match(/msie\s(\d+)/) || [])[1] || '11' // 由于 ie11 并没有 msie 的标识
: false;
}();
/**
* 获取 layui 常见方法,以便用于组件单独版
*/
lay.layui = layui || {};
lay.getPath = layui.cache.dir; // 获取当前 JS 所在目录
lay.stope = layui.stope; // 中止冒泡
lay.each = function () {
// 遍历
layui.each.apply(layui, arguments);
return this;
};
/**
* 数字前置补零
* @param {number | string} num - 原始数字
* @param {number} [length=2] - 数字长度,如果原始数字长度小于 length则前面补零
* @returns {string} 返回补 0 后的数字
* @example
* ```js
* lay.digit(6, 2); // "06"
* lay.digit('7', 3); // "007"
* ```
*/
lay.digit = function (num, length) {
if (!(typeof num === 'string' || typeof num === 'number')) return '';
var str = '';
num = String(num);
length = length || 2;
for (var i = num.length; i < length; i++) {
str += '0';
}
return num < Math.pow(10, length) ? str + num : num;
};
/**
* 创建元素
* @param {string} elemName - 元素的标签名
* @param {Object.<string, string>} [attr] - 添加到元素上的属性
* @returns {HTMLElement} 返回创建的 HTML 元素
* @example
* ```js
* lay.elem('div', {id: 'test'}) // <div id="test"></div>
* ```
*/
lay.elem = function (elemName, attr) {
var elem = document.createElement(elemName);
lay.each(attr || {}, function (key, value) {
elem.setAttribute(key, value);
});
return elem;
};
/**
* 当前页面是否存在滚动条
* @returns {boolean} 是否存在滚动条
* @example
* ```
* lay.hasScrollbar() // true 或 false
* ```
*/
lay.hasScrollbar = function () {
return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight);
};
/**
* 自动递增器,一般用于组件自增索引
*/
lay.autoIncrementer = function (key, opts = {}) {
const {
target = document.body
} = opts;
const name = '_LAY_AUTOINCREMENTER_ID_';
const incrementer = target[name] = target[name] || {};
incrementer[key] = incrementer[key] || 0;
return ++incrementer[key];
};
/**
* 获取 style rules
* @param {HTMLStyleElement} style - HTMLStyle 元素
* @param {(ruleItem: CSSStyleRule, index: number) => boolean} [callback] - 用来返回 style 元素中的每个 `style rule` 的函数,返回 true 终止遍历
* @returns {CSSRuleList} 返回 `style rules`
* @example
* ```
* <style id="test">
* .lay-card{
* color: #000;
* }
* .lay-btn-success{
* color: green;
* }
* </style>
*
* lay.getStyleRules($('#test')[0], function(rule, index){
* if(rule.selectorText === '.lay-card'){
* console.log(index, rule.cssText) // 0 '.lay-card{color: #000}'
* rule.style.color = '#EEE';
* return true; // 终止遍历
* }
* }) // RuleList
* ```
*/
lay.getStyleRules = function (style, callback) {
if (!style) return;
var sheet = style.sheet || style.styleSheet || {};
var rules = sheet.cssRules || sheet.rules;
if (typeof callback === 'function') {
layui.each(rules, function (i, item) {
if (callback(item, i)) return true;
});
}
return rules;
};
/**
* 创建 style 样式
* @param {Object} options - 可配置的选项
* @param {string | HTMLElement | JQuery} [options.target] - 目标容器,指定后会将样式追加到目标容器
* @param {string} [options.id] - 样式元素的 id默认自增
* @param {string} options.text - 样式内容
* @returns {HTMLStyleElement} 返回创建的样式元素
* @example
* ```html
* <div id="targetEl">
* <!-- 样式追加到目标容器 -->
* <style id="LAY-STYLE-DF-0">.card{color: #000}</style>
* </div>
*
* lay.style({
* target: '#targetEl',
* text: '.card{color: #000}'
* }) // <style id="LAY-STYLE-DF-0">.card{color: #000}</style>
* ```
*/
lay.style = function (options) {
options = options || {};
var style = lay.elem('style');
var styleText = options.text || '';
var target = options.target;
if (!styleText) return;
// 添加样式
if ('styleSheet' in style) {
style.setAttribute('type', 'text/css');
style.styleSheet.cssText = styleText;
} else {
style.innerHTML = styleText;
}
// ID
style.id = 'LAY-STYLE-' + (options.id || function (index) {
lay.style.index++;
return 'DF-' + index;
}(lay.style.index || 0));
// 是否向目标容器中追加 style 元素
if (target) {
var styleElem = lay(target).find('#' + style.id);
styleElem[0] && styleElem.remove();
lay(target).append(style);
}
return style;
};
/**
* 将元素定位到指定目标元素附近
* @param {HTMLElement} target - 目标元素
* @param {HTMLElement} elem - 定位元素
* @param {Object} [opts] - 可配置的选项
* @param {'absolute' | 'fixed'} [opts.position] - 元素的定位类型
* @param {'left' | 'right'} [opts.clickType="left"] - 点击类型,默认为 'left',如果 {@link target} 是 document 或 body 元素,则为 'right'
* @param {'left' | 'right' | 'center'} [opts.align="left"] - 对齐方式
* @param {boolean} [opts.allowBottomOut=false] - 顶部没有足够区域显示时,是否允许底部溢出
* @param {string | number} [opts.margin=5] - 边距
* @param {Event} [opts.e] - 事件对象,仅右键生效
* @param {boolean} [opts.SYSTEM_RELOAD] - 是否重载,用于出现滚动条时重新计算位置
* @param {[offsetX:number, offsetY:number]} [opts.offset] - 相对于触发元素的额外偏移量[x,y]
* @example
* ```js
* <button id="targetEl">dropdown</button>
* <ul id="contentEl" class="dropdown-menu">
* <li>菜单1</li>
* <li>菜单2</li>
* </ul>
*
* // 下拉菜单将被定位到按钮附近
* lay.position(
* $('#targetEl')[0],
* $('#contentEl')[0],
* {
* position: 'fixed',
* align: 'center'
* }
* )
* ```
*/
lay.position = function (target, elem, opts) {
if (!elem) return;
opts = opts || {};
// 如果绑定的是 document 或 body 元素,则直接获取鼠标坐标
if (target === document || target === lay('body')[0]) {
opts.clickType = 'right';
}
// 绑定绑定元素的坐标
var rect = opts.clickType === 'right' ? function () {
var e = opts.e || window.event || {};
return {
left: e.clientX,
top: e.clientY,
right: e.clientX,
bottom: e.clientY
};
}() : target.getBoundingClientRect();
var elemWidth = elem.offsetWidth; // 控件的宽度
var elemHeight = elem.offsetHeight; // 控件的高度
// 滚动条高度
var scrollArea = function (type) {
type = type ? 'scrollLeft' : 'scrollTop';
return document.body[type] | document.documentElement[type];
};
// 窗口宽高
var winArea = function (type) {
return document.documentElement[type ? 'clientWidth' : 'clientHeight'];
};
var margin = 'margin' in opts ? opts.margin : 5;
var left = rect.left;
var top = rect.bottom;
// 相对元素居中
if (opts.align === 'center') {
left = left - (elemWidth - target.offsetWidth) / 2;
} else if (opts.align === 'right') {
left = left - elemWidth + target.offsetWidth;
}
// 判断右侧是否超出边界
if (left + elemWidth + margin > winArea('width')) {
left = winArea('width') - elemWidth - margin; // 如果超出右侧,则将面板向右靠齐
}
// 左侧是否超出边界
if (left < margin) left = margin;
// 判断底部和顶部是否超出边界
if (rect.bottom + elemHeight + margin > winArea()) {
// 底部超出边界
// 优先判断顶部是否有足够区域显示完全,且底部不能超出边界
if (rect.top > elemHeight + margin && rect.top <= winArea()) {
top = rect.top - elemHeight - margin * 2; // 顶部有足够的区域显示
} else if (!opts.allowBottomOut) {
// 顶部没有足够区域显示时,是否允许底部溢出
top = winArea() - elemHeight - margin * 2; // 面板向底部靠齐
if (top < 0) top = 0; // 如果面板底部靠齐时,又溢出窗口顶部,则只能将顶部靠齐
}
}
/*
if(top + elemHeight + margin > winArea()){
// 优先顶部是否有足够区域显示完全
if(rect.top > elemHeight + margin){
top = rect.top - elemHeight - margin*2; // 顶部有足够的区域显示
} else {
// 如果面板是鼠标右键弹出,且顶部没有足够区域显示,则将面板向底部靠齐
if(obj.clickType === 'right'){
top = winArea() - elemHeight - margin*2;
if(top < 0) top = 0; // 不能溢出窗口顶部
} else {
top = margin; // 位置计算逻辑完备性处理
}
}
}
*/
// 定位类型
var position = opts.position;
if (position) elem.style.position = position;
var offsetX = opts.offset ? opts.offset[0] : 0;
var offsetY = opts.offset ? opts.offset[1] : 0;
// 设置坐标
elem.style.left = left + (position === 'fixed' ? 0 : scrollArea(1)) + offsetX + 'px';
elem.style.top = top + (position === 'fixed' ? 0 : scrollArea()) + offsetY + 'px';
// 防止页面无滚动条时,又因为弹出面板而出现滚动条导致的坐标计算偏差
if (!lay.hasScrollbar()) {
var rect1 = elem.getBoundingClientRect();
// 如果弹出面板的溢出窗口底部,则表示将出现滚动条,此时需要重新计算坐标
if (!opts.SYSTEM_RELOAD && rect1.bottom + margin > winArea()) {
opts.SYSTEM_RELOAD = true;
setTimeout(function () {
lay.position(target, elem, opts);
}, 50);
}
}
};
/**
* 获取元素上的属性配置项
* @param {string | HTMLElement | JQuery} elem - HTML 元素
* @param {{attr: string} | string} [opts="lay-options"] - 可配置的选项string 类型指定属性名
* @returns {Object.<string, any>} 返回元素上的属性配置项
* @example
* ```js
* <div id="testEl" lay-options="{color:red}" lay-toc="{hot: true}"></div>
*
* var elem = $('#testEl')
* lay.options(elem) // {color:red}
* lay.options(elem[0]) // {color:red}
* lay.options('#testEl') // {color:red}
* lay.options('#testEl', {attr: 'lay-toc'}) // {hot: true}
* lay.options('#testEl', 'lay-toc') // {hot: true}
*
* $('#testEl').attr('lay-toc') // '{hot: true}'
* ```
*/
lay.options = function (elem, opts) {
opts = typeof opts === 'object' ? opts : {
attr: opts
};
if (elem === document) return {};
var othis = lay(elem);
var attrName = opts.attr || 'lay-options';
var attrValue = othis.attr(attrName);
try {
/**
* 请注意: 开发者在使用 lay-options="{}" 配置组件选项时,需确保属性值不来自于网页用户,
* 即属性值必须在网页开发者自身的可控范围内,否则请勿在 HTML 标签属性中获取组件选项。
*/
return new Function('return ' + (attrValue || '{}'))();
} catch (ev) {
layui.hint().error(opts.errorText || [attrName + '="' + attrValue + '"', '\n parseerror: ' + ev].join('\n'), 'error');
return {};
}
};
/**
* 元素是否属于顶级元素document 或 body
* @param {HTMLElement} elem - HTML 元素
* @returns {boolean} 是否属于顶级元素
* @example
* ```js
* lay.isTopElem(document) // true
* ```
*/
lay.isTopElem = function (elem) {
var topElems = [document, lay('body')[0]],
matched = false;
lay.each(topElems, function (index, item) {
if (item === elem) {
return matched = true;
}
});
return matched;
};
// 剪切板
lay.clipboard = {
/**
* 写入文本
* @param {Object} options - 可配置的选项
* @param {string} options.text - 写入剪贴板的文本
* @param {() => void} [options.done] - 写入成功/完成回调
* @param {(err?: any) => void} [options.error] - 写入失败回调
* @example
* ```js
* lay.clipboard.writeText({
* text: '测试文本',
* done: function(){ layer.msg('copied')},
* error: function(){ layer.msg('error')}
* })
* ```
*/
writeText: function (options) {
var text = String(options.text);
if (navigator && 'clipboard' in navigator) {
navigator.clipboard.writeText(text).then(options.done, function () {
legacyCopy();
});
} else {
legacyCopy();
}
function legacyCopy() {
var elem = document.createElement('textarea');
elem.value = text;
elem.style.position = 'fixed';
elem.style.opacity = '0';
elem.style.top = '0px';
elem.style.left = '0px';
document.body.appendChild(elem);
elem.select();
try {
document.execCommand('copy');
typeof options.done === 'function' && options.done();
} catch (err) {
typeof options.error === 'function' && options.error(err);
} finally {
elem.remove ? elem.remove() : document.body.removeChild(elem);
}
}
}
};
/**
* 检测是否支持 Passive Event Listeners
* 引用自 https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
* @type {boolean}
*/
lay.passiveSupported = function () {
var passiveSupported = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function () {
passiveSupported = true;
return passiveSupported;
}
});
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
} catch {
// ignore
}
return passiveSupported;
}();
/**
* 是否支持 touch 事件
*/
lay.touchEventsSupported = function () {
return 'ontouchstart' in window;
};
/**
* @typedef touchSwipeState
* @prop {{x: number,y: number}} pointerStart - 初始坐标
* @prop {{x: number,y: number}} pointerEnd - 结束坐标
* @prop {number} distanceX - X 轴移动距离
* @prop {number} distanceY - Y 轴移动距离
* @prop {'none'|'right'|'left'|'up'|'down'} direction - 滑动方向
* @prop {Date} timeStart 开始时间
*/
/**
* @callback touchSwipeCallback
* @param {TouchEvent} e 滑动事件
* @param {touchSwipeState} state 滑动相关的状态
*/
/**
* 基于 touch 事件的触摸滑动
* @param {string | HTMLElement | JQuery} elem - HTML 元素
* @param {{onTouchStart?: touchSwipeCallback; onTouchMove?: touchSwipeCallback; onTouchEnd?: touchSwipeCallback; preventDefault?: boolean}} opts - 配置项
*/
lay.touchSwipe = function (elem, opts) {
var options = opts;
var targetElem = lay(elem)[0];
var preventDefault = 'preventDefault' in options ? options.preventDefault : true;
if (!targetElem || !lay.touchEventsSupported()) return;
var state = {
pointerStart: {
x: 0,
y: 0
},
pointerEnd: {
x: 0,
y: 0
},
distanceX: 0,
distanceY: 0,
direction: 'none',
// 'up','down','left','right','none
timeStart: null
};
var onStart = function (e) {
if (e.touches.length !== 1) return;
bindEvents();
// 重置状态
state.timeStart = Date.now();
state.pointerStart.x = state.pointerEnd.x = e.touches[0].clientX;
state.pointerStart.y = state.pointerEnd.y = e.touches[0].clientY;
state.distanceX = state.distanceY = 0;
state.direction = 'none';
options.onTouchStart && options.onTouchStart(e, state);
};
var onMove = function (e) {
if (preventDefault) {
e.preventDefault();
}
state.pointerEnd.x = e.touches[0].clientX;
state.pointerEnd.y = e.touches[0].clientY;
state.distanceX = state.pointerStart.x - state.pointerEnd.x;
state.distanceY = state.pointerStart.y - state.pointerEnd.y;
if (Math.abs(state.distanceX) > Math.abs(state.distanceY)) {
state.direction = state.distanceX > 0 ? 'left' : 'right';
} else {
state.direction = state.distanceY > 0 ? 'up' : 'down';
}
options.onTouchMove && options.onTouchMove(e, state);
};
var onEnd = function (e) {
options.onTouchEnd && options.onTouchEnd(e, state);
unbindEvents();
};
var bindEvents = function () {
targetElem.addEventListener('touchmove', onMove, lay.passiveSupported ? {
passive: false
} : false);
targetElem.addEventListener('touchend', onEnd);
targetElem.addEventListener('touchcancel', onEnd);
};
var unbindEvents = function () {
targetElem.removeEventListener('touchmove', onMove);
targetElem.removeEventListener('touchend', onEnd, lay.passiveSupported ? {
passive: false
} : false);
targetElem.removeEventListener('touchcancel', onEnd);
};
// 防止事件重复绑定
if (targetElem.__lay_touchswipe_cb_) {
targetElem.removeEventListener('touchstart', targetElem.__lay_touchswipe_cb_);
}
targetElem.__lay_touchswipe_cb_ = onStart;
targetElem.addEventListener('touchstart', onStart);
};
/** @type {(elem: Element|Document|Window,eventName: string,fn:EventListenerOrEventListenerObject,options: boolean | AddEventListenerOptions) => any}*/
lay.addEvent = function () {
if (document.addEventListener) {
return function (elem, eventName, fn, options) {
elem.addEventListener(eventName, fn, options);
};
} else {
return function (elem, eventName, fn) {
var prefix = '_lay_on_';
var eventsCacheName = prefix + eventName;
var listener = function (e) {
e.target = e.srcElement;
fn.call(elem, e);
};
listener._rawFn = fn;
if (!elem[eventsCacheName]) {
elem[eventsCacheName] = [];
}
var include = false;
lay.each(elem[eventsCacheName], function (_, listener) {
if (listener._rawFn === fn) {
include = true;
return true;
}
});
if (!include) {
elem[eventsCacheName].push(listener);
elem.attachEvent('on' + eventName, listener);
}
};
}
}();
/** @type {(elem: Element|Document|Window,eventName: string,fn:EventListenerOrEventListenerObject,options: boolean | EventListenerOptions) => any}*/
lay.removeEvent = function () {
if (document.removeEventListener) {
return function (elem, eventName, fn, options) {
elem.removeEventListener(eventName, fn, options);
};
} else {
return function (elem, eventName, fn) {
var prefix = '_lay_on_';
var eventsCacheName = prefix + eventName;
var events = elem[eventsCacheName];
if (layui.isArray(events)) {
var newEvents = [];
lay.each(events, function (_, listener) {
if (listener._rawFn === fn) {
elem.detachEvent('on' + eventName, listener);
} else {
newEvents.push(listener);
}
});
elem[eventsCacheName] = newEvents;
}
};
}
}();
/**
* 绑定指定元素外部的点击事件
* @param {HTMLElement} target - 响应事件的元素
* @param {(e: Event) => void} handler - 事件触发时执行的函数
* @param {object} [options] - 选项
* @param {string} [options.event="pointerdown"] - 事件类型
* @param {HTMLElement | Window} [options.scope=document] - 事件范围
* @param {Array<HTMLElement | string>} [options.ignore] - 忽略触发事件的元素或选择器字符串
* @param {boolean} [options.capture=true] - 对内部事件 listener 使用捕获阶段
* @param {boolean} [options.detectIframe] - 是否检测 iframe
* @returns {() => void} - 返回一个停止事件响应的函数
*/
lay.onClickOutside = function (target, handler, options) {
options = options || {};
var eventType = options.event || ('onpointerdown' in window ? 'pointerdown' : 'mousedown');
var scopeTarget = options.scope || document;
var ignore = options.ignore || [];
var useCapture = 'capture' in options ? options.capture : true;
var detectIframe = options.detectIframe;
var listener = function (event) {
var el = target;
var eventTarget = event.target || event.srcElement;
var eventPath = getEventPath(event);
if (!el || el === eventTarget || eventPath.indexOf(el) !== -1) {
return;
}
if (shouldIgnore(event, eventPath)) {
return;
}
handler(event);
};
function shouldIgnore(event, eventPath) {
var eventTarget = event.target || event.srcElement;
for (var i = 0; i < ignore.length; i++) {
var target = ignore[i];
if (typeof target === 'string') {
var targetElements = document.querySelectorAll(target);
for (var j = 0; j < targetElements.length; j++) {
var targetEl = targetElements[i];
if (targetEl === eventTarget || eventPath.indexOf(targetEl) !== -1) {
return true;
}
}
} else {
if (target && (target === eventTarget || eventPath.indexOf(target) !== -1)) {
return true;
}
}
}
}
function getEventPath(event) {
var path = event.composedPath && event.composedPath() || event.path;
var eventTarget = event.target || event.srcElement;
if (path !== null && path !== undefined) {
return path;
}
function getParents(node, memo) {
memo = memo || [];
var parentNode = node.parentNode;
return parentNode ? getParents(parentNode, memo.concat([parentNode])) : memo;
}
return [eventTarget].concat(getParents(eventTarget));
}
function bindEventListener(elem, eventName, handler, opts) {
elem.addEventListener ? elem.addEventListener(eventName, handler, opts) : elem.attachEvent('on' + eventName, handler);
return function () {
elem.removeEventListener ? elem.removeEventListener(eventName, handler, opts) : elem.detachEvent('on' + eventName, handler);
};
}
var cleanup = [bindEventListener(scopeTarget, eventType, listener, lay.passiveSupported ? {
passive: true,
capture: useCapture
} : useCapture), detectIframe && bindEventListener(window, 'blur', function (event) {
setTimeout(function () {
if (document.activeElement && document.activeElement.tagName === 'IFRAME' && target.contains && !target.contains(document.activeElement)) {
handler(event);
}
}, 0);
})];
return function () {
for (var i = 0; i < cleanup.length; i++) {
cleanup[i] && cleanup[i]();
}
cleanup = null;
};
};
/**
* 检查对象是否具有指定的属性
* @param {Record<string, any>} obj 要检查的对象
* @param {string} prop 要检查的属性名
* @returns {boolean} 如果对象具有指定的属性,则为 true否则为 false
*/
lay.hasOwn = function (obj, prop) {
return hasOwnProperty.call(obj, prop);
};
/**
* 转义 HTML 字符串中的特殊字符
* @param {string} html 要转义的 HTML 字符串
* @returns {string} 转义后的 HTML 字符串
*/
lay.escape = function (html) {
var exp = /[<"'>]|&(?=#?[a-zA-Z0-9]+)/g;
if (html === undefined || html === null) return '';
html += '';
if (!exp.test(html)) return html;
return html.replace(/&(?=#?[a-zA-Z0-9]+;?)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#39;').replace(/"/g, '&quot;');
};
/**
* 还原转义的 HTML 字符串中的特殊字符
* @param {string} html 要还原转义的 HTML 字符串
* @returns {string} 还原转义后的 HTML 字符串
*/
lay.unescape = function (html) {
if (html === undefined || html === null) return '';
return String(html).replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
};
/**
* 生成唯一的 ID 字符串
* @param {string} prefix ID 前缀,默认为'id'
* @returns {string} 唯一 ID 字符串
*/
var generateUniqueId = function () {
var counter = 0;
var lastTimestamp = null;
return function (prefix) {
prefix = prefix || 'id';
var timestamp = new Date().getTime();
// 如果时间戳与上一次相同,增加计数器
// 否则重置计数器
if (timestamp === lastTimestamp) {
counter++;
} else {
counter = 0;
lastTimestamp = timestamp;
}
// 结合时间戳、随机数和计数器生成 ID
var random = Math.floor(Math.random() * 10000);
return prefix + '-' + timestamp + '-' + random + '-' + counter;
};
}();
/**
* 创建共享的 ResizeObserver 实例
* @param {string} namespace 命名空间,用于区分不同的 ResizeObserver 实例
* @returns {ResizeObserver | null} ResizeObserver 实例或 null如果不支持
*/
lay.createSharedResizeObserver = function (namespace) {
if (typeof window.ResizeObserver === 'undefined') {
console.warn('ResizeObserver is not supported in this browser.');
return null;
}
namespace = namespace || '';
var ATTR_NAME = 'lay-' + namespace + '-resizeobserver-key';
var handlerCache = {};
var o = new ResizeObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
var attrValue = entries[i].target.getAttribute(ATTR_NAME);
if (attrValue) {
var callback = handlerCache[attrValue];
if (typeof callback === 'function') {
callback(entries[i]);
}
}
}
});
return Object.freeze({
observe: function (element, callback) {
if (!element || !(element instanceof Element)) {
console.warn('createSharedResizeObserver: Cannot observe non-Element.');
return;
}
var attrValue = element.getAttribute(ATTR_NAME);
if (!attrValue) {
attrValue = generateUniqueId(namespace);
element.setAttribute(ATTR_NAME, attrValue);
}
// 使用同一个观察者实例多次观察同一个元素,不会重复添加
handlerCache[attrValue] = callback;
o.observe(element);
},
unobserve: function (element) {
if (!element || !(element instanceof Element)) {
console.warn('createSharedResizeObserver: Cannot unobserve non-Element.');
return;
}
var attrValue = element.getAttribute(ATTR_NAME);
if (!attrValue) {
return;
}
// 清除相关回调
if (handlerCache[attrValue]) {
delete handlerCache[attrValue];
}
element.removeAttribute(ATTR_NAME);
o.unobserve(element);
},
disconnect: function () {
for (var key in handlerCache) {
if (lay.hasOwn(handlerCache, key)) {
delete handlerCache[key];
var elem = document.querySelector('[' + ATTR_NAME + '="' + key + '"]');
if (elem) {
elem.removeAttribute(ATTR_NAME);
}
}
}
o.disconnect();
}
});
};
/**
* 树状数据转平铺
* @param {Object[]} data - 树状数据
* @param {Object} options - 可选项
* @param {string} [options.childrenKey='children'] - 子节点字段名
* @param {string} [options.idKey='id'] - 节点 id 字段名
* @param {string} [options.parentKey='parentId'] - 父节点 id 字段名
* @param {boolean} [options.keepChildren=true] - 是否保留子节点数据
* @returns {Object[]} 返回平铺数据
*/
lay.treeToFlat = function (data, options) {
options = Object.assign({
childrenKey: 'children',
idKey: 'id',
parentKey: 'parentId',
keepChildren: true
}, options);
// 展平
var toFlat = function (initData, nodes, parentId) {
return nodes.reduce(function (acc, currNode) {
var children = currNode[options.childrenKey];
if (!options.keepChildren) {
delete currNode[options.childrenKey];
}
currNode[options.parentKey] = parentId; // 设置父节点 id
acc.push(currNode);
// 递归子节点
if (children && children.length) {
return toFlat(acc, children, currNode[options.idKey]);
}
return acc;
}, initData);
};
return toFlat([], JSON.parse(JSON.stringify(data)), null);
};
/**
* 平铺数据转树状
* @param {Array} data - 平铺数据
* @param {Object} options - 可选项
* @param {string} [options.childrenKey='children'] - 子节点字段名
* @param {string} [options.idKey='id'] - 节点 id 字段名
* @param {string} [options.parentKey='parentId'] - 父节点 id 字段名
*/
lay.flatToTree = function (data, options) {
options = Object.assign({
childrenKey: 'children',
idKey: 'id',
parentKey: 'parentId'
}, options);
data = JSON.parse(JSON.stringify(data)); // 深拷贝,防止修改原数据
// 先创建节点映射,确保无论平铺数据的顺序如何,组装树时都能正确匹配
var map = data.reduce(function (acc, currNode) {
var id = currNode[options.idKey];
acc[id] = currNode;
acc[id][options.childrenKey] = [];
return acc;
}, {});
// 组装树
return data.reduce(function (acc, currNode) {
var id = currNode[options.idKey];
var parentId = currNode[options.parentKey];
// 根节点
if (parentId === null || !map[parentId]) {
acc.push(map[id]);
} else {
// 子节点
map[parentId][options.childrenKey].push(currNode);
}
return acc;
}, []);
};
/*
* lay 元素操作
*/
// 追加字符
Class.addStr = function (str, new_str) {
str = str.replace(/\s+/, ' ');
new_str = new_str.replace(/\s+/, ' ').split(' ');
lay.each(new_str, function (ii, item) {
if (!new RegExp('\\b' + item + '\\b').test(str)) {
str = str + ' ' + item;
}
});
return str.replace(/^\s|\s$/, '');
};
// 移除值
Class.removeStr = function (str, new_str) {
str = str.replace(/\s+/, ' ');
new_str = new_str.replace(/\s+/, ' ').split(' ');
lay.each(new_str, function (ii, item) {
var exp = new RegExp('\\b' + item + '\\b');
if (exp.test(str)) {
str = str.replace(exp, '');
}
});
return str.replace(/\s+/, ' ').replace(/^\s|\s$/, '');
};
// 查找子元素
Class.fn.find = function (selector) {
// var that = this;
var elem = [];
var isObject = typeof selector === 'object';
this.each(function (i, item) {
var children = isObject && item.contains(selector) ? selector : item.querySelectorAll(selector || null);
lay.each(children, function (index, child) {
elem.push(child);
});
});
return lay(elem);
};
// 元素遍历
Class.fn.each = function (fn) {
return lay.each.call(this, this, fn);
};
// 添加 className
Class.fn.addClass = function (className, type) {
return this.each(function (index, item) {
item.className = Class[type ? 'removeStr' : 'addStr'](item.className, className);
});
};
// 移除 className
Class.fn.removeClass = function (className) {
return this.addClass(className, true);
};
// 是否包含 css 类
Class.fn.hasClass = function (className) {
var has = false;
this.each(function (index, item) {
if (new RegExp('\\b' + className + '\\b').test(item.className)) {
has = true;
}
});
return has;
};
// 添加或获取 css style
Class.fn.css = function (key, value) {
var that = this;
var parseValue = function (v) {
return isNaN(v) ? v : v + 'px';
};
return typeof key === 'string' && value === undefined ? function () {
if (that.length > 0) return that[0].style[key];
}() : that.each(function (index, item) {
typeof key === 'object' ? lay.each(key, function (thisKey, thisValue) {
item.style[thisKey] = parseValue(thisValue);
}) : item.style[key] = parseValue(value);
});
};
// 添加或获取宽度
Class.fn.width = function (value) {
var that = this;
return value === undefined ? function () {
if (that.length > 0) return that[0].offsetWidth; // 此处还需做兼容
}() : that.each(function () {
that.css('width', value);
});
};
// 添加或获取高度
Class.fn.height = function (value) {
var that = this;
return value === undefined ? function () {
if (that.length > 0) return that[0].offsetHeight; // 此处还需做兼容
}() : that.each(function () {
that.css('height', value);
});
};
// 添加或获取属性
Class.fn.attr = function (key, value) {
var that = this;
return value === undefined ? function () {
if (that.length > 0) return that[0].getAttribute(key);
}() : that.each(function (index, item) {
item.setAttribute(key, value);
});
};
// 移除属性
Class.fn.removeAttr = function (key) {
return this.each(function (index, item) {
item.removeAttribute(key);
});
};
// 设置或获取 HTML 内容
Class.fn.html = function (html) {
var that = this;
return html === undefined ? function () {
if (that.length > 0) return that[0].innerHTML;
}() : this.each(function (index, item) {
item.innerHTML = html;
});
};
// 设置或获取值
Class.fn.val = function (value) {
var that = this;
return value === undefined ? function () {
if (that.length > 0) return that[0].value;
}() : this.each(function (index, item) {
item.value = value;
});
};
// 追加内容
Class.fn.append = function (elem) {
return this.each(function (index, item) {
typeof elem === 'object' ? item.appendChild(elem) : item.innerHTML = item.innerHTML + elem;
});
};
// 移除内容
Class.fn.remove = function (elem) {
return this.each(function (index, item) {
elem ? item.removeChild(elem) : item.parentNode.removeChild(item);
});
};
// 事件绑定
Class.fn.on = function (eventName, fn, options) {
return this.each(function (index, item) {
lay.addEvent(item, eventName, fn, options);
});
};
// 解除事件
Class.fn.off = function (eventName, fn, options) {
return this.each(function (index, item) {
lay.removeEvent(item, eventName, fn, options);
});
};
export { lay };