mirror of
https://github.com/layui/layui.git
synced 2026-02-09 02:09:18 +08:00
1207 lines
37 KiB
JavaScript
1207 lines
37 KiB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, ''').replace(/"/g, '"');
|
||
};
|
||
|
||
/**
|
||
* 还原转义的 HTML 字符串中的特殊字符
|
||
* @param {string} html 要还原转义的 HTML 字符串
|
||
* @returns {string} 还原转义后的 HTML 字符串
|
||
*/
|
||
lay.unescape = function (html) {
|
||
if (html === undefined || html === null) return '';
|
||
return String(html).replace(/"/g, '"').replace(/'/g, "'").replace(/>/g, '>').replace(/</g, '<').replace(/&/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 };
|