ph-utils
Version:
js 开发工具集,前后端都可以使用(commonjs和es module)
636 lines (635 loc) • 22.6 kB
JavaScript
export function elem(selector, dom) {
if (typeof selector === "string") {
return (dom || document).querySelectorAll(selector);
}
else {
return [selector];
}
}
/**
* 根据选择器获取 DOM 元素。
* @param selector - 选择器字符串或 HTMLElement 实例。
* @param dom - 可选参数,指定在哪个 DOM 节点下查找元素,默认为 document。
* @returns 返回匹配到的 HTMLElement 实例。
*/
export function $(selector, dom) {
return elem(selector, dom);
}
/**
* 创建一个 HTML 元素,支持通过标签名或 HTML 字符串创建。
* @param tag - 元素标签名或 HTML 字符串。
* @param option - 元素的属性、样式、文本内容等配置。所有不是指定的属性,都会通过 setAttribute 设置
* @param ctx - 元素的父级文档上下文。
* @returns 创建的 HTML 元素。
*/
export function create(tag, option = {}, children) {
let $el;
if (tag.startsWith("<") && tag.endsWith(">")) {
const parser = new DOMParser();
const doc = parser.parseFromString(tag, "text/html");
$el = doc.body.firstElementChild;
}
else {
$el = document.createElement(tag);
}
if ($el) {
if (option) {
for (const key in option) {
const value = option[key];
if (value != null) {
if (key === "class") {
$el.className = formatClass(value);
}
else if (key === "style") {
$el.style.cssText = formatStyle(value);
}
else if (key === "textContent" && value) {
$el.textContent = value;
}
else if (key === "innerHTML" && value) {
$el.innerHTML = value;
}
else if (key === "outerHTML" && value) {
$el.outerHTML = value;
}
else {
if (typeof value === "boolean" && value === true) {
$el.setAttribute(key, "");
}
else {
$el.setAttribute(key, value);
}
}
}
}
}
if (children) {
if (children instanceof HTMLElement || children instanceof DocumentFragment) {
$el.appendChild(children);
}
else {
let len = children.length;
const fragment = document.createDocumentFragment();
for (let i = 0; i < len; i++) {
const child = children[i];
if (child instanceof HTMLElement) {
fragment.appendChild(child);
}
}
if (len > 0) {
$el.appendChild(fragment);
}
}
}
}
return $el;
}
/** 创建节点 */
export function $$(tag, option = {}, children) {
return create(tag, option, children);
}
/**
* 根据选择器获取匹配的第一个 DOM 元素。
* @param selector - 选择器字符串或直接的 HTMLElement。
* @param dom - 可选的父级 DOM 元素,默认为当前文档。
* @returns 返回匹配的第一个 HTMLElement,如果没有找到则返回 null。
*/
export function $one(selector, dom) {
if (typeof selector === "string") {
return (dom || document).querySelector(selector);
}
return selector;
}
/**
* 为节点添加 class
* @param {HTMLElement} elem 待添加 class 的节点
* @param {string} clazz 需要添加的 class
*/
export function addClass(elem, clazz) {
elem.classList.add(clazz);
}
/**
* 节点移除 class
* @param {HTMLElement} elem 待移除 class 的节点
* @param {string} clazz 需要移除的 class
*/
export function removeClass(elem, clazz) {
elem.classList.remove(clazz);
}
/**
* 判断节点是否包含某个 class
* @param elem 待判断 class 的节点
* @param clazz 待判断的 class
* @returns
*/
export function hasClass(elem, clazz) {
return elem.classList.contains(clazz);
}
/**
* 切换指定元素的类名。
* 如果元素已包含该类名,则移除它;否则添加它。
* @param el - 要操作的 HTML 元素。
* @param clazz - 要切换的类名。
*/
export function toggleClass(el, clazz) {
if (hasClass(el, clazz)) {
removeClass(el, clazz);
}
else {
addClass(el, clazz);
}
}
/**
* 替换指定元素的 CSS 类
*
* 该函数用于根据给定的旧类名或类索引,将其替换为新类名。
* 如果旧类不存在,则不会进行替换。
*
* @param el - 目标 HTML 元素,将对其类进行操作
* @param oldClazz - 要替换的旧类名或类索引(基于数字时对应 classList 的索引位置)
* @param newClazz - 新的类名,用于替换旧的类
* @returns void
*/
export function replaceClass(el, oldClazz, newClazz) {
// 判断 oldClazz 是否为数字,若是则通过索引从 el.classList 中获取对应类名,否则直接使用字符串值
let old = typeof oldClazz === "number" ? el.classList.item(oldClazz) : oldClazz;
// 如果旧类存在,则使用 classList.replace 方法进行替换
if (old) {
el.classList.replace(old, newClazz);
}
}
/**
* 为节点添加事件处理
* @param {HTMLElement} element 添加事件的节点
* @param {string} listener 事件名称
* @param {function} fn 事件处理函数
* @param {boolean} option 是否是只运行一次的处理函数或者配置,其中 eventFlag 为 string,如果配置该项,则表明为委托事件
*/
export function on(element, listener, fn, option) {
if (element.length != null) {
iterate(element, (elem) => {
if (typeof option === "object" && option.eventFlag) {
elem.setAttribute(option.eventFlag, "__stop__");
}
elem.addEventListener(listener, fn, option);
});
}
else if (element) {
if (typeof option === "object" && option.eventFlag) {
element.setAttribute(option.eventFlag, "__stop__");
}
element.addEventListener(listener, fn, option);
}
}
/**
* 移除指定元素的事件监听器。
* @param el - 要移除监听器的 HTML 元素。
* @param listener - 事件名称。
* @param fn - 要移除的事件监听器函数。
*/
export function off(el, listener, fn, option) {
if (el.length != null) {
iterate(el, (elem) => {
elem.removeEventListener(listener, fn, option);
});
}
else if (el) {
el.removeEventListener(listener, fn, option);
}
}
/**
* 判断事件是否应该继续传递。
* 从事件目标开始向上遍历DOM树,检查每个节点上是否存在指定的属性。
* 如果找到该属性且其值不为'__stop__',则返回true,表示事件可以继续传递。
* 否则,返回false,表示事件应该停止传递。
* 通常用于事件委托时判断是否继续传递事件。
*
* @param e - 触发的事件对象
* @param eventFlag - 需要检查的属性名
* @param endRoot - 可选,如果传递该参数,则表示停止遍历的节点,如果未传递,则表示遍历到文档根节点为止
* @returns 包含三个元素的数组:[是否继续传递事件, 属性值, 当前检查的DOM节点]
*/
export function shouldEventNext(e, eventFlag, endRoot) {
let target = e.target;
let flag = "";
do {
if ((endRoot && endRoot.isSameNode(target)) || target.tagName === "BODY") {
break;
}
if (target.getAttribute) {
flag = target.getAttribute(eventFlag) || "";
}
if (flag === "") {
target = target.parentNode;
}
if (!target)
break;
} while (flag === "");
return [flag !== "__stop__" && flag !== "", flag, target];
}
/**
* 设置或获取节点的 innerHTML 属性
* @param element
* @param htmlstr 可选,如果传递该参数,则表示设置;否则表示获取
* @returns
*/
export function html(element, htmlstr) {
if (htmlstr == null) {
return element.innerHTML;
}
else {
element.innerHTML = htmlstr;
return undefined;
}
}
/**
* 设置或获取节点的 textContent 属性
* @param element
* @param textstr 可选,如果传递该参数,则表示设置;否则表示获取
* @returns
*/
export function text(element, textstr) {
if (textstr == null) {
return element.textContent;
}
else {
element.textContent = textstr;
return undefined;
}
}
/**
* 节点列表遍历
* @param elems
* @param fn 遍历到节点时的回调,回调第一个参数为遍历到的节点,第2个参数为 index;如果回调函数返回 true,则会终止遍历(break)
*/
export function iterate(elems, fn) {
for (let i = 0, len = elems.length; i < len; i++) {
let r = fn(elems[i], i);
if (r === true) {
break;
}
}
}
/**
* 设置或获取节点 data-* 属性
* @param elem
* @param key data- 后面跟随的值
* @param value 如果传递该值表示获取;否则表示设置
* @returns
*/
export function attr(elem, key, value) {
if (value != null) {
elem.setAttribute("data-" + key, value);
}
else {
return elem.getAttribute("data-" + key);
}
}
export function getAttr(el, key, defaultValue) {
const value = el.getAttribute(key);
if (defaultValue == null)
return value;
const valueType = typeof defaultValue;
if (value == null)
return defaultValue;
// 类型转换
if (valueType === "bigint" || valueType === "number") {
if (value === "")
return defaultValue;
return Number(value);
}
if (valueType === "boolean") {
if (value === "" || value === "1" || value === "true" || value === key) {
return true;
}
return false;
}
if (valueType === "object") {
if (value === "")
return defaultValue;
return JSON.parse(value);
}
return value;
}
/**
* 获取指定节点的父节点
* @param el
* @returns
*/
export function parent(el) {
return el.parentNode;
}
/**
* 获取隐藏节点的尺寸, 如果 parent 传空, 且 hideNode 为节点,则会通过修改原始样式方式计算
* @param {string | HTMLElement} hideNode - The node to hide.
* @param parent - 添加临时节点的父节点, 如果传递 null, 则通过修改原始样式方式计算,默认为: body.
* @returns The DOMRect of the element.
*/
export function queryHideNodeSize(hideNode, parent = document.body) {
if (parent == null && typeof hideNode !== "string") {
// 保存原来的样式
const originalDisplay = hideNode.style.display;
const originalVisibility = hideNode.style.visibility;
const originalPosition = hideNode.style.position;
// 设置为可见但不可见状态,不影响布局
hideNode.style.position = "absolute";
hideNode.style.visibility = "hidden";
hideNode.style.display = "block";
// 读取高度
const rect = hideNode.getBoundingClientRect();
// 恢复原样式
hideNode.style.display = originalDisplay;
hideNode.style.visibility = originalVisibility;
hideNode.style.position = originalPosition;
return { width: rect.width, height: rect.height };
}
// 计算折叠菜单的高度
let $tmp = document.createElement("div");
$tmp.style.cssText = "position:fixed;left:-1000px;top:-1000px;opacity:0;";
let $tmpInner = document.createElement("div");
$tmpInner.style.cssText = "position:relative;";
if (typeof hideNode === "string") {
$tmpInner.innerHTML = hideNode;
}
else {
$tmpInner.appendChild(hideNode.cloneNode(true));
}
$tmp.appendChild($tmpInner);
(parent || document.body).appendChild($tmp);
let rect = $tmpInner.children[0].getBoundingClientRect();
(parent || document.body).removeChild($tmp);
return { width: rect.width, height: rect.height };
}
/**
* 判断元素是否在父元素的可视区域内。
*
* @param el 要检查的元素
* @param parent 元素的父元素,默认为document.body
* @param direction 检查的方向,默认为"horizontal"
* @returns 如果元素在父元素的可视区域内,则返回true;否则返回false。
*/
export function isVisible(el, parent = null, direction = "horizontal") {
if (parent == null) {
parent = el.offsetParent;
}
// 获取父元素的边界信息
const containerRect = parent.getBoundingClientRect();
// 获取元素的边界信息
const elementRect = el.getBoundingClientRect();
// 根据检查方向,确定元素的起始和结束位置
// 元素的上、下边界
let elStart = direction === "horizontal" ? elementRect.left : elementRect.top;
let elEnd = direction === "horizontal" ? elementRect.right : elementRect.bottom;
// 根据检查方向,确定父元素的起始和结束位置
// 容器的可视区域的上、下边界
let containerStart = direction === "horizontal" ? containerRect.left : containerRect.top;
let containerEnd = direction === "horizontal" ? containerRect.right : containerRect.bottom;
// 判断元素是否在父元素的可视区域内
// 判断元素是否完全在容器的可视区域内
return elStart >= containerStart && elEnd <= containerEnd;
}
/**
* 判断当前设备是否为移动设备。
*
* 本函数通过检查用户代理字符串和屏幕尺寸来判断设备是否为移动设备。
* 这对于需要根据设备类型进行不同布局或功能调整的应用非常有用。
*
* @returns {boolean} 如果设备是移动设备,则返回true;否则返回false。
*/
export function isMobile() {
// 通过正则表达式匹配用户代理字符串,判断是否为移动设备
// 检查是否为移动设备
const isMobile = /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// 获取窗口的 innerWidth 和 innerHeight,用于判断屏幕尺寸
// 获取屏幕宽度和高度
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
// 判断屏幕宽度或高度是否小于等于800或600,用于进一步确认是否为移动设备
const isScreenMobile = screenWidth <= 800 || screenHeight <= 600;
// 如果是移动设备或屏幕尺寸符合移动设备特征,则返回true
return isMobile || isScreenMobile;
}
/**
* 格式化类名,支持数组和对象两种形式。
* - 数组形式:数组中的每个元素代表一个类名,非空元素将被添加到结果字符串中。
* - 对象形式:对象的键代表类名,值为真(非空、非undefined、非null)时,键将被添加到结果字符串中。
* @param classObj - 类名对象或数组
* @returns 格式化后的类名字符串
*/
export function formatClass(classObj) {
let classes = "";
if (Array.isArray(classObj)) {
for (let i = 0, len = classObj.length; i < len; i++) {
const item = classObj[i];
if (item) {
classes += `${item} `;
}
}
}
else if (typeof classObj === "string") {
classes = classObj;
}
else {
for (const key in classObj) {
if (classObj[key]) {
classes += `${key} `;
}
}
}
return classes.trim();
}
/**
* 将样式对象格式化为 CSS 样式字符串。
* @param styleObj - 样式对象,可以是字符串数组或键值对对象。
* @returns 格式化后的 CSS 样式字符串。
*/
export function formatStyle(styleObj) {
let styleStr = "";
if (Array.isArray(styleObj)) {
for (let i = 0, len = styleObj.length; i < len; i++) {
const item = styleObj[i];
if (item) {
styleStr += `${item};`;
}
}
}
else if (typeof styleObj === "string") {
styleStr = styleObj;
}
else {
for (const key in styleObj) {
const value = styleObj[key];
if (value) {
styleStr += `${key}:${value};`;
}
}
}
return styleStr;
}
function toggleCssProperty(el, properties, method = "set") {
for (let i = 0, len = properties.length; i < len; i++) {
const rec = properties[i];
if (method === "set") {
el.style.setProperty(rec[0], rec[1]);
}
else {
el.style.removeProperty(rec[0]);
}
}
}
/**
* 执行元素的过渡动画。
*
* 如果是名称类似于 Vue.js 的过渡动画,通过添加 `*-active`、`*-from|to` class 类名。
*
* @param el - 需要执行过渡动画的 HTML 元素。
* @param nameOrProperties - 过渡动画的名称或属性数组。可以是字符串表示的动画名称,或者是包含属性名称、初始值或目标值、持续时间。eg. [['opacity', '0.5']] | 'nt-opacity'
* @param dir - 过渡动画的方向,"leave" 表示离开,"enter" 表示进入。默认值为 "enter"。
* @param finish - 过渡动画结束时的回调函数。
*
* @example <caption>执行 `nt-opacity` 名称动画</caption>
* // css
* .nt-opacity-enter-active,
* .nt-opacity-leave-active {
* transition: opacity 0.3s ease;
* }
* .nt-opacity-enter-from,
* .nt-opacity-leave-to {
* opacity: 0;
* }
* // js
* transition($el, "nt-opacity", "enter");
*
* @example <caption>执行 `style` 属性动画</caption>
* transition($el, [["opacity", "0", "0.3s"]], "enter");
*
* @example <caption>动画结束后移除节点</caption>
* transition($el, [["opacity", "0", "0.3s"]], "leave", () => { $el.remove(); });
*/
export function transition(el, nameOrProperties, dir = "enter", finish) {
const p = dir === "enter" ? "from" : "to";
let nameClass = "", activeClass = "";
/** 动画状态, -1 - 准备, 0 - 进行中, 1 - 完成 */
let status = -1;
const trans = [];
if (typeof nameOrProperties === "string") {
nameClass = `${nameOrProperties}-${dir}-${p}`;
activeClass = `${nameOrProperties}-${dir}-active`;
}
else {
for (let i = 0, len = nameOrProperties.length; i < len; i++) {
const rec = nameOrProperties[i];
if (rec.length >= 3) {
trans.push(`${rec[0]} ${rec[2]}`);
}
}
}
status = 0;
if (dir === "enter") {
if (nameClass) {
el.classList.add(nameClass);
setTimeout(() => {
el.classList.add(activeClass);
requestAnimationFrame(() => {
el.classList.remove(nameClass);
});
}, 0);
}
else {
toggleCssProperty(el, nameOrProperties, "set");
setTimeout(() => {
if (trans.length > 0) {
el.style.setProperty("transition", trans.join(", "));
}
requestAnimationFrame(() => {
toggleCssProperty(el, nameOrProperties, "remove");
});
}, 0);
}
}
else {
if (nameClass) {
el.classList.add(activeClass);
requestAnimationFrame(() => {
el.classList.add(nameClass);
});
}
else {
if (trans.length > 0) {
el.style.setProperty("transition", trans.join(", "));
}
requestAnimationFrame(() => {
toggleCssProperty(el, nameOrProperties, "set");
});
}
}
el.addEventListener("transitionend", () => {
if (status === 0) {
status = 1;
if (nameClass) {
el.classList.remove(activeClass);
requestAnimationFrame(() => {
el.classList.remove(nameClass);
});
}
else {
if (trans) {
el.style.removeProperty("transition");
}
requestAnimationFrame(() => {
toggleCssProperty(el, nameOrProperties, "remove");
});
}
if (finish) {
finish();
}
}
}, { once: true });
}
/**
* 计算光标位置
*
* 根据输入值的变化计算新的光标位置,主要用于输入框内容变化时的光标位置调整
*
* @param start - 光标起始位置,通常为: input.selectionStart
* @param end - 光标结束位置(通常与起始位置相同,用于选择范围),通常为: input.selectionEnd
* @param oldValue - 变化前的值
* @param newValue - 变化后的值
* @returns 新的光标位置对象,包含新的起始和结束位置
*
* @example
* // 删除字符时,光标向前移动
* calcuteCursorPosition(2, 2, "abc", "ac"); // 返回 { start: 1, end: 1 }
*
* @example
* // 添加字符时,光标向后移动
* calcuteCursorPosition(2, 2, "ac", "abc"); // 返回 { start: 3, end: 3 }
*
* @example
* // 替换字符时,保持位置
* calcuteCursorPosition(2, 2, "abc", "axc"); // 返回 { start: 2, end: 2 }
*/
export function calcuteCursorPosition(start, end, oldValue, newValue) {
// 5. 【关键】计算新光标位置并恢复
let newStart = start;
let newEnd = end;
// 简单策略:如果新值比旧值短,说明删了字符,光标前移
// 更健壮的做法:对比差异,但通常可简化处理
if (newValue.length < oldValue.length) {
// 例如:用户在中间删了一个非法字符
newStart = Math.max(0, start - (oldValue.length - newValue.length));
newEnd = newStart;
}
else if (newValue.length > oldValue.length) {
// 插入合法字符,光标通常就在插入点后,可保持原偏移
// 但需防止超出长度
newStart = Math.min(newValue.length, start + (newValue.length - oldValue.length));
newEnd = newStart;
}
else {
// 长度不变(如替换),保持原位置(但要限制范围)
newStart = Math.min(newValue.length, start);
newEnd = newStart;
}
return { start: newStart, end: newEnd };
}