type ParseWrapper = [number, string, string] var parseFix: { [name: string]: ParseWrapper } var parseContainer: Element /** * 解析一段 HTML 并创建相应的节点,如果 HTML 片段中有多个根节点则返回一个文档片段 * @param html 要解析的 HTML 片段 * @param context 节点所属的文档 * @example parse("
Hello world
") //
Hello world
*/ export function parse(html: string, context = document) { if (!parseFix) { const select: ParseWrapper = [1, ``] const table: ParseWrapper = [1, ``, `
`] const tr: ParseWrapper = [3, ``, `
`] parseFix = { __proto__: null!, option: select, optgroup: select, thead: table, tbody: table, tfoot: table, caption: table, colgroup: table, tr: [2, ``, `
`], col: [2, ``, `
`], td: tr, th: tr, legend: [1, `
`, `
`], area: [1, ``, ``], param: [1, ``, ``] } parseContainer = document.createElement("div") } let container = context === document ? parseContainer : context.createElement("div") const match = /^<(\w+)/.exec(html) const wrapper = match && parseFix[match[1].toLowerCase()] if (wrapper) { container.innerHTML = wrapper[1] + html + wrapper[2] for (let level = wrapper[0]; level--;) { container = container.lastChild as Element } } else { container.innerHTML = html } let node = (container.firstChild || context.createTextNode(html)) as Element | Text | DocumentFragment if (node.nextSibling) { node = context.createDocumentFragment() while (container.firstChild) { node.appendChild(container.firstChild) } } return node } /** * 在指定节点范围内查找 CSS 选择器匹配的所有元素 * @param parent 要查找的根节点 * @param selector 要查找的 CSS 选择器 * @example queryAll(document.body, ".class") */ export function query(parent: Element | Document | NodeSelector, selector: string): HTMLElement[] /** * 在整个文档内查找 CSS 选择器匹配的所有元素 * @param selector 要查找的 CSS 选择器 * @return 返回匹配的元素列表 */ export function query(selector: string): HTMLElement[] export function query(parent: Element | Document | NodeSelector | string, selector?: string) { return Array.prototype.slice.call(querySelector(parent, selector), 0) } /** * 在指定节点范围内查找 CSS 选择器匹配的所有元素 * @param parent 要查找的根节点 * @param selector 要查找的 CSS 选择器 * @example findAll(document.body, ".class") */ export function findAll(parent: Element | Document | NodeSelector, selector: string): Element[] /** * 在整个文档内查找 CSS 选择器匹配的所有元素 * @param selector 要查找的 CSS 选择器 * @example findAll(".class") */ export function findAll(selector: string): Element[] export function findAll(parent: Element | Document | NodeSelector | string, selector?: string) { return Array.prototype.slice.call(querySelector(parent, selector), 0) } /** * 在指定节点范围内查找 CSS 选择器匹配的第一个元素 * @param parent 要查找的根节点 * @param selector 要查找的 CSS 选择器 * @example find(document.body, ".class") */ export function find(parent: Element | Document | NodeSelector, selector: string): Element | null /** * 在整个文档内查找 CSS 选择器匹配的第一个元素 * @param selector 要查找的 CSS 选择器 * @example find(".class") */ export function find(selector: string): Element | null export function find(parent: Element | Document | NodeSelector | string, selector?: string) { return querySelector(parent, selector, true) } var idSeed: number | undefined function querySelector(parent: Element | Document | NodeSelector | string, selector?: string, first?: boolean) { if (typeof parent === "string") { selector = parent parent = document } try { return first ? parent.querySelector(selector!) : parent.querySelectorAll(selector!) } catch (e) { if ((parent as Node).nodeType === 1 && selector!.charCodeAt(0) === 62/*>*/) { let idCreated: boolean | undefined selector = `#${(parent as Element).id || (idCreated = true, (parent as Element).id = "__dom_q" + (idSeed = idSeed! + 1 || 1) + "__")} ${selector}` try { return first ? parent.querySelector(selector) : parent.querySelectorAll(selector) } catch (e) { } finally { if (idCreated) { (parent as Element).id = "" } } } throw e } } /** * 判断元素是否匹配指定的 CSS 选择器 * @param elem 要处理的元素 * @param selector 要判断的 CSS 选择器 * @param context 选择器的上下文 * @example match(document.body, "body") // true */ export function match(elem: Element, selector: string, context?: Element | Document | null): boolean { if (elem.matches) { try { return elem.matches(selector) } catch (e) { } } let parent = elem.parentNode as Element if (parent) { return Array.prototype.indexOf.call(querySelector(context || parent, selector), elem) >= 0 } parent = elem.ownerDocument!.documentElement! try { parent.appendChild(elem) return match(elem, selector, context) } finally { parent.removeChild(elem) } } /** * 获取节点的第一个子元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 */ export function first(node: Node, selector?: string) { return walk(node, selector, "nextSibling", "firstChild") } /** * 获取节点的最后一个子元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 */ export function last(node: Node, selector?: string) { return walk(node, selector, "previousSibling", "lastChild") } /** * 获取节点的下一个相邻元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 */ export function next(node: Node, selector?: string) { return walk(node, selector, "nextSibling") } /** * 获取节点的上一个相邻元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 */ export function prev(node: Node, selector?: string) { return walk(node, selector, "previousSibling") } /** * 获取节点的父元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 */ export function parent(node: Node, selector?: string) { return walk(node, selector, "parentNode") } function walk(node: Node, selector: string | undefined, nextProp: "nextSibling" | "previousSibling" | "parentNode", firstProp: typeof nextProp | "firstChild" | "lastChild" = nextProp) { for (node = node[firstProp]!; node; node = node[nextProp]!) { if (node.nodeType === 1 && (!selector || match(node as Element, selector))) { break } } return node as Element | null } /** * 从指定节点开始向父元素查找第一个匹配指定 CSS 选择器的元素 * @param node 要处理的节点 * @param selector 要匹配的 CSS 选择器 * @param context 如果提供了上下文则只在指定的元素范围内搜索,否则在整个文档查找 * @example closest(document.body, "body") */ export function closest(node: Node, selector: string, context?: Element | Document | null) { while (node && node !== context && (node.nodeType !== 1 || !match(node as Element, selector, context))) { node = node.parentNode! } return node === context ? null : node as Element } /** * 获取节点的所有直接子元素 * @param node 要处理的节点 * @param selector 用于筛选元素的 CSS 选择器 * @example children(document.body) */ export function children(node: Node, selector?: string) { const nodes: Element[] = [] for (node = node.firstChild!; node; node = node.nextSibling!) { if (node.nodeType === 1 && (!selector || match(node as Element, selector))) { nodes.push(node as Element) } } return nodes } /** * 判断节点是否等同于或包含另一个节点 * @param node 要处理的节点 * @param child 要判断的子节点 * @example contains(document.body, document.body) // true */ export function contains(node: Node, child: Node) { if (node.contains) { return node.contains(child) } for (; child; child = child.parentNode!) { if (child === node) { return true } } return false } /** * 获取节点在其父节点所有直接子元素中的索引(从 0 开始) * @param node 要处理的节点 */ export function index(node: Node) { let r = 0 while ((node = node.previousSibling!)) { if (node.nodeType === 1) { r++ } } return r } /** * 在节点末尾插入一段 HTML 或一个节点,返回插入的新节点 * @param node 要处理的节点 * @param content 要插入的 HTML 或节点 */ export function append(node: Node, content: string | Node | null) { return insert(node, content, false, false) } /** * 在节点开头插入一段 HTML 或一个节点,返回插入的新节点 * @param node 要处理的节点 * @param content 要插入的 HTML 或节点 */ export function prepend(node: Node, content: string | Node | null) { return insert(node, content, true, false) } /** * 在节点前插入一段 HTML 或一个节点,返回插入的新节点 * @param node 要处理的节点该节点必须具有父节点 * @param content 要插入的 HTML 或节点 */ export function before(node: Node, content: string | Node | null) { return insert(node, content, true, true) } /** * 在节点后插入一段 HTML 或一个节点,返回插入的新节点 * @param node 要处理的节点该节点必须具有父节点 * @param content 要插入的 HTML 或节点 */ export function after(node: Node, content: string | Node | null) { return insert(node, content, false, true) } function insert(node: Node, content: string | Node | null, prepend: boolean, sibling: boolean) { if (content == null) { return null } if (typeof content !== "object") { content = parse(content, node.ownerDocument || node as Document) } if (sibling) { return node.parentNode!.insertBefore(content, prepend ? node : node.nextSibling) } return prepend ? node.insertBefore(content, node.firstChild) : node.appendChild(content) } /** * 从文档中移除节点 * @param node 要移除的节点 */ export function remove(node: Node | null) { node && node.parentNode && node.parentNode.removeChild(node) } /** * 复制节点及其子节点,返回复制的节点 * @param node 要复制的节点 */ export function clone(node: T) { return node.cloneNode(true) as T } /** * 获取元素的属性值,如果属性不存在则返回 null * @param elem 要处理的元素 * @param attrName 要获取的属性名(使用骆驼规则,如 `readOnly`) * @example getAttr(document.body, "class") */ export function getAttr(elem: Element, attrName: string | keyof Element): any { return attrName in elem ? elem[attrName as keyof Element] : elem.getAttribute(attrName) } /** * 设置元素的属性值 * @param elem 要处理的元素 * @param attrName 要设置的属性名(使用骆驼规则,如 `readOnly`) * @param value 要设置的属性值设置为 null 表示删除属性 * @example setAttr(document.body, "class", "red") * @example setAttr(document.body, "class", null) */ export function setAttr(elem: Element, attrName: string | keyof Element, value: any) { if (value != null && typeof value !== "string" || attrName in elem && !/^on./.test(attrName)) { if (value == null && typeof elem[attrName as keyof Element] === "string") { value = "" } try { (elem as any)[attrName] = value } catch (e) { // IE Edge: elem.colSpan = 0 抛出异常 if (attrName === "colSpan") { (elem as any).colSpan = +value > 0 ? +value : 1 } else { throw e } } } else if (value == null) { elem.removeAttribute(attrName) } else { elem.setAttribute(attrName, value) } } /** * 获取元素的文本内容,如果是输入框则返回其输入的值 * @param elem 要处理的元素 * @example getText(document.body) */ export function getText(elem: Element) { return (elem as HTMLInputElement)[textProp(elem)]! } /** * 设置元素的文本内容,如果是输入框则设置其输入的值 * @param elem 要处理的元素 * @param value 要设置的文本内容 * @example setText(document.body, "text") */ export function setText(elem: Element, value: string) { (elem as HTMLInputElement)[textProp(elem)] = value } function textProp(elem: Element) { return /^(INPUT|SELECT|TEXTAREA)$/.test(elem.tagName) ? "value" : "textContent" } /** * 获取元素的内部的 HTML * @param elem 要处理的元素 * @example getHtml(document.body) */ export function getHtml(elem: Element) { return elem.innerHTML } /** * 设置元素的内部的 HTML * @param elem 要处理的元素 * @param value 要设置的内部 HTML * @example setHtml(document.body, "html") */ export function setHtml(elem: Element, value: string) { elem.innerHTML = value } /** * 获取元素的内部的 HTML * @param elem 要处理的元素 * @example getHtml(document.body) */ export function getHTML(elem: Element) { return elem.innerHTML } /** * 设置元素的内部的 HTML * @param elem 要处理的元素 * @param value 要设置的内部 HTML * @example setHtml(document.body, "html") */ export function setHTML(elem: Element, value: string) { elem.innerHTML = value } /** * 判断元素是否已添加指定的 CSS 类名 * @param elem 要处理的元素 * @param className 要判断的 CSS 类名(只能有一个) */ export function hasClass(elem: Element, className: string) { return (" " + elem.className + " ").indexOf(" " + className + " ") >= 0 } /** * 添加元素的 CSS 类名 * @param elem 要处理的元素 * @param className 要添加的 CSS 类名(只能有一个) * @example addClass(document.body, "light") */ export function addClass(elem: Element, className: string) { toggleClass(elem, className, true) } /** * 删除元素的 CSS 类名 * @param elem 要处理的元素 * @param className 要删除的 CSS 类名(只能有一个) * @example removeClass(document.body, "light") */ export function removeClass(elem: Element, className: string) { toggleClass(elem, className, false) } /** * 如果存在(不存在)则删除(添加)元素的 CSS 类名 * @param elem 要处理的元素 * @param className 要添加或删除的 CSS 类名(只能有一个) * @param value 如果为 true 则强制添加 CSS 类名,如果为 false 则强制删除 CSS 类名 * @example toggleClass(document.body, "light") */ export function toggleClass(elem: Element, className: string, value?: boolean) { if (hasClass(elem, className)) { if (!value) { elem.className = (" " + elem.className + " ").replace(" " + className + " ", " ").trim() } } else if (value === undefined || value) { elem.className = elem.className ? elem.className + " " + className : className } } /** * 返回已添加当前浏览器特定的样式前缀(如 "webkit-")的 CSS 属性名 * @param propName 要处理的 CSS 属性名 * @example vendor("transform") */ export function vendor(propName: string) { if (!(propName in document.documentElement!.style)) { const capName = propName.charAt(0).toUpperCase() + propName.slice(1) for (const prefix of ["webkit", "Moz", "ms", "O"]) { if ((prefix + capName) in document.documentElement!.style) { return prefix + capName } } } return propName } /** * 获取元素的实际的 CSS 属性值 * @param elem 要处理的元素 * @param propName 要获取的 CSS 属性名(使用骆驼规则,如 "fontSize") * @example getStyle(document.body, "fontSize") */ export function getStyle(elem: Element, propName: string) { return elem.ownerDocument!.defaultView!.getComputedStyle(elem)[vendor(propName) as any] } /** * 设置元素的 CSS 属性值 * @param elem 要处理的元素 * @param propName 要设置的 CSS 属性名(使用骆驼规则,如 "fontSize") * @param value 要设置的 CSS 属性值如果是数字则自动追加像素单位 * @example setStyle(document.body, "fontSize") */ export function setStyle(elem: ElementCSSInlineStyle, propName: string, value: string | number | null) { elem.style[vendor(propName) as any] = value && typeof value === "number" && !/^(?:columnCount|fillOpacity|flexGrow|flexShrink|fontWeight|lineHeight|opacity|order|orphans|widows|zIndex|zoom)$/.test(propName) ? value + "px" : value as any } /** * 计算一个元素所有 CSS 属性值的和(如果是长度则以像素为单位) * @param elem 要计算的元素 * @param propNames 要计算的 CSS 属性名(使用骆驼规则,如 `fontSize`)列表 * @example computeStyle(document.body, "fontSize", "lineHeight") */ export function computeStyle(elem: Element, ...propNames: string[]) { let value = 0 const computedStyle = elem.ownerDocument!.defaultView!.getComputedStyle(elem) for (const propName of propNames) { value += parseFloat(computedStyle[propName as any]) || 0 } return value } /** 表示一个坐标 */ export interface Point { /** 相对于屏幕左上角的水平距离(单位为像素)*/ x: number /** 相对于屏幕左上角的垂直距离(单位为像素)*/ y: number } /** 表示一个尺寸 */ export interface Size { /** 宽度(单位为像素)*/ width: number /**高度(单位为像素)*/ height: number } /** 表示一个矩形区域 */ export interface Rect extends Point, Size { } /** * 获取元素的滚动距离,如果元素不可滚动则返回原点 * @param elem 要处理的元素或文档 * @example getScroll(document.body) */ export function getScroll(elem: Element | Document) { if (elem.nodeType === 9) { const win = (elem as Document).defaultView! if ("pageXOffset" in win) { return { x: win.pageXOffset, y: win.pageYOffset } as Point } elem = (elem as Document).documentElement! } return { x: (elem as Element).scrollLeft, y: (elem as Element).scrollTop } as Point } /** * 设置元素的滚动距离 * @param elem 要处理的元素或文档 * @param value 要设置的坐标允许只设置部分属性 * @example setScroll(document.body, { x: 100, y: 500 }) */ export function setScroll(elem: Element | Document, value: Partial) { if (elem.nodeType === 9) { (elem as Document).defaultView!.scrollTo( (value.x == null ? getScroll(elem) : value).x!, (value.y == null ? getScroll(elem) : value).y! ) } else { if (value.x != null) (elem as Element).scrollLeft = value.x if (value.y != null) (elem as Element).scrollTop = value.y } } /** * 获取元素和其定位父元素的偏移距离,如果元素未设置定位信息则返回原点 * @param elem 要处理的元素 * @example getOffset(document.body) */ export function getOffset(elem: HTMLElement) { const left = getStyle(elem, "left") const top = getStyle(elem, "top") if ((left && top && left !== "auto" && top !== "auto") || getStyle(elem, "position") !== "absolute") { return { x: parseFloat(left) || 0, y: parseFloat(top) || 0 } as Point } const parent = offsetParent(elem)! const rect = getRect(elem) if (parent.nodeName !== "HTML") { const rootRect = getRect(parent) rect.x -= rootRect.x rect.y -= rootRect.y } rect.x -= computeStyle(elem, "marginLeft") + computeStyle(parent, "borderLeftWidth") rect.y -= computeStyle(elem, "marginTop") + computeStyle(parent, "borderTopWidth") return rect as Point } /** * 设置元素和其定位父元素的偏移距离 * @param elem 要处理的元素 * @param value 要设置的坐标允许只设置部分属性 * @example setOffset(document.body, { x: 100 }) */ export function setOffset(elem: HTMLElement, value: Partial) { if (value.x! >= 0) { elem.style.left = value.x + "px" } if (value.y! >= 0) { elem.style.top = value.y + "px" } } /** * 获取元素的定位父元素 * @param elem 要处理的元素 * @example offsetParent(document.body) */ export function offsetParent(elem: HTMLElement) { let node = elem while ((node = node.offsetParent as HTMLElement) && node.nodeName !== "HTML" && getStyle(node, "position") === "static") { return node || elem.ownerDocument!.documentElement } return node } /** * 获取元素实际占用的区域(包括内边距和边框、不包括外边距),如果元素不可见则返回空区域 * @param elem 要处理的元素或文档 * @example getRect(document.body) */ export function getRect(elem: Element | Document) { const doc = elem.ownerDocument || elem as Document const html = doc.documentElement! const rect = getScroll(doc) as Rect if (elem.nodeType === 9) { rect.width = Math.max((elem as Document).defaultView!.innerWidth, html.clientWidth) rect.height = Math.max((elem as Document).defaultView!.innerHeight, html.clientHeight) } else { const elemRect = (elem as Element).getBoundingClientRect() rect.x += elemRect.left - html.clientLeft rect.y += elemRect.top - html.clientTop rect.width = elemRect.width rect.height = elemRect.height } return rect } /** * 设置元素的区域 * @param elem 要处理的元素 * @param value 要设置的区域(包括内边距和边框、不包括外边距),允许只设置部分属性 * @example setRect(document.body, {width: 200, height: 400}) */ export function setRect(elem: HTMLElement, value: Partial) { const style = elem.style if (value.x != null || value.y != null) { movable(elem) const currentPosition = getRect(elem) const offset = getOffset(elem) if (value.y != null) { style.top = offset.y + value.y - currentPosition.y + "px" } if (value.x != null) { style.left = offset.x + value.x - currentPosition.x + "px" } } if (value.width != null || value.height != null) { const boxSizing = getStyle(elem, "boxSizing") === "border-box" if (value.width != null) { style.width = value.width - (boxSizing ? 0 : computeStyle(elem, "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight")) + "px" } if (value.height != null) { style.height = value.height - (boxSizing ? 0 : computeStyle(elem, "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom")) + "px" } } } /** * 确保指定的元素可移动 * @param elem 要处理的元素 */ export function movable(elem: HTMLElement) { if (!/^(?:abs|fix)/.test(getStyle(elem, "position"))) { elem.style.position = "relative" } } interface EventFix { [key: string]: any /** 实际绑定的事件 */ bind?: string /** 委托时实际绑定的事件 */ delegate?: string /** 实际绑定的事件触发后筛选是否触发当前事件的过滤器 */ filter?: (this: EventFix, e: Event, elem: Element | Document) => boolean | void /** 自定义绑定事件的函数 */ add?: (this: EventFix, elem: Element | Document, eventHandler: EventListener, eventOptions: AddEventListenerOptions | boolean) => void /** 自定义解绑事件的函数 */ remove?: (this: EventFix, elem: Element | Document, eventHandler: EventListener, eventOptions: AddEventListenerOptions | boolean) => void } var eventFix: { [event: string]: EventFix } var defaultEventOptions: false | AddEventListenerOptions = false /** * 绑定事件 * @param elem 要处理的元素或文档 * @param eventName 要绑定的事件名 * @param selector 要委托的目标元素的 CSS 选择器 * @param eventHandler 要绑定的事件处理函数 * @param thisArg 执行事件处理函数时 this 的值 * @param eventOptions 事件附加选项 * @example on(document.body, "mouseenter", "a", e => { this.firstChild.innerHTML = e.pageX }) */ export function on(elem: Element | Document | Window, eventName: string, selector: string, eventHandler: (e: Event, sender: HTMLElement) => void, thisArg?: any, eventOptions?: AddEventListenerOptions): void /** * 绑定事件 * @param elem 要处理的元素或文档 * @param eventName 要绑定的事件名 * @param eventHandler 要绑定的事件处理函数 * @param thisArg 执行事件处理函数时 this 的值 * @param eventOptions 事件附加选项 * @example on(document.body, "click", e => { alert("clicked") }) */ export function on(elem: Element | Document | Window, eventName: string, eventHandler: (e: Event, sender: any) => void, thisArg?: any, eventOptions?: AddEventListenerOptions): void export function on(elem: Element | Document, eventName: string, selector: string | typeof eventHandler, eventHandler?: ((e: Event, sender?: any) => void) | typeof thisArg, thisArg?: any, eventOptions?: any) { if (!eventFix) { // 检测是否支持 {passive: true} const opt = { get passive() { defaultEventOptions = { passive: false } return true } } as any document.addEventListener("__passive__", null!, opt) document.removeEventListener("__passive__", null!, opt) const isEnterOrLeave = (e: MouseEvent, sender: Element | Document) => e.type.length === 12 || e.type.length === 10 && e.type.charCodeAt(0) === 109 /*m*/ || !contains(sender, e.relatedTarget as Node) eventFix = { __proto__: null!, // focus/blur 不支持冒泡,委托时使用 foucin/foucsout focus: { delegate: "focusin" }, blur: { delegate: "focusout" }, // mouseenter/mouseleave 不支持冒泡,委托时使用 mouseover/mouseout mouseenter: { delegate: "mouseover", filter: isEnterOrLeave }, mouseleave: { delegate: "mouseout", filter: isEnterOrLeave }, // pointerenter/pointerleave 不支持冒泡,委托时使用 pointerover/pointerout pointerenter: { delegate: "pointerover", filter: isEnterOrLeave }, pointerleave: { delegate: "pointerout", filter: isEnterOrLeave }, mouseclick: { bind: "click" }, } if ((window as any).mozInnerScreenX !== undefined) { // FF 51-:不支持 focusin/focusout 事件 const focusAdd: EventFix["add"] = function (elem, eventHandler) { elem.addEventListener(this.bind!, eventHandler, true) } const focusRemove: EventFix["remove"] = function (elem, eventHandler) { elem.removeEventListener(this.bind!, eventHandler, true) } eventFix.focusin = { bind: "focus", add: focusAdd, remove: focusRemove } eventFix.focusout = { bind: "blur", add: focusAdd, remove: focusRemove } // FF:右击也会触发 click 事件 eventFix.click = { filter: (e: MouseEvent) => !e.button } } const html = Document.prototype // FF:不支持 mousewheel 事件 if (!("onmousewheel" in html)) { eventFix.mousewheel = { bind: "DOMMouseScroll", filter(e: MouseWheelEvent) { // 统一使用 wheelDelta 获取滚轮距离 (e as any).wheelDelta = -(e.detail || 0) / 3 } } } // FF、CH 30-:不支持 mouseenter/mouseleave 事件 if (!("onmouseenter" in html)) { eventFix.mouseenter.bind = "mouseover" eventFix.mouseleave.bind = "mouseout" } // 低版本浏览器:不支持 auxclick 事件 if (!("onauxclick" in html)) { eventFix.auxclick = { bind: "mouseup", filter: (e: MouseEvent) => e.button === 2 } } // 低版本浏览器:不支持 pointer* 事件 if (!("onpointerdown" in html)) { eventFix.pointerover = { bind: "mouseover" } eventFix.pointerout = { bind: "mouseout" } eventFix.pointerenter.bind = eventFix.mouseenter.bind || "mouseenter" eventFix.pointerenter.delegate = "mouseover" eventFix.pointerleave.bind = eventFix.mouseleave.bind || "mouseleave" eventFix.pointerleave.delegate = "mouseout" eventFix.pointerdown = { bind: "mousedown" } eventFix.pointerup = { bind: "mouseup" } eventFix.pointermove = { bind: "mousemove" } } // 触屏:适配鼠标事件 if ((window as any).TouchEvent) { // 将触摸事件模拟成鼠标事件 Object.defineProperty(TouchEvent.prototype, "button", { value: 1, configurable: true, enumerable: true }) for (const prop of ["pageX", "pageY", "clientX", "clientY", "screenX", "screenY"]) { Object.defineProperty(TouchEvent.prototype, prop, { get(this: TouchEvent) { return (this.changedTouches[0] as any)[prop] }, configurable: true, enumerable: true }) } const requireClick = (elem: HTMLElement) => { switch (elem.tagName.toUpperCase()) { case "INPUT": switch ((elem as HTMLInputElement).type) { case "button": return false } return !(elem as HTMLInputElement).disabled && !(elem as HTMLInputElement).readOnly case "TEXTAREA": case "SELECT": case "LABEL": case "IFRAME": case "VIDEO": case "AUDIO": case "PROGRESS": case "SUMMARY": return true default: return false } } // 解决部分设备 click 300ms 延时问题 eventFix.click = { add(elem, eventHandler) { let state: any = 0 elem.addEventListener("touchstart", (eventHandler as any).__touchStart__ = (e: TouchEvent) => { if (e.changedTouches.length === 1) { state = [e.changedTouches[0].pageX, e.changedTouches[0].pageY] } }, false) elem.addEventListener("touchend", (eventHandler as any).__touchEnd__ = (e: TouchEvent) => { if (state && e.changedTouches.length === 1 && Math.pow(e.changedTouches[0].pageX - state[0], 2) + Math.pow(e.changedTouches[0].pageY - state[1], 2) < 25) { state = 1 // 禁用非特定情况的事件防止点击穿透 if (!requireClick(e.target as any) && !requireClick(elem as any)) { e.preventDefault() } eventHandler.call(elem, e) } }, false) elem.addEventListener("click", (eventHandler as any).__click__ = (e: MouseEvent) => { const trigger = state !== 1 state = 0 trigger && eventHandler.call(elem, e) }, false) }, remove(elem, eventHandler) { elem.removeEventListener("touchstart", (eventHandler as any).__touchStart__, false) elem.removeEventListener("touchend", (eventHandler as any).__touchEnd__, false) elem.removeEventListener("click", (eventHandler as any).__click__, false) } } const pointerAdd: EventFix["add"] = function (elem, eventHandler, eventOptions: any) { let state = 0 elem.addEventListener((this as any).touch, (eventHandler as any).__touch__ = function (e: MouseEvent) { state = 1 eventHandler.call(this, e) }, eventOptions) elem.addEventListener(this.bind!, (eventHandler as any).__mouse__ = function (e: MouseEvent) { if (state) { state = 0 } else { eventHandler.call(this, e) } }, eventOptions) } const pointerRemove: EventFix["remove"] = function (elem, eventHandler, eventOptions: any) { elem.removeEventListener((this as any).touch, (eventHandler as any).__touch__, eventOptions) elem.removeEventListener(this.bind!, (eventHandler as any).__mouse__, eventOptions) } // CH: 虽然 Chrome 支持 pointer 事件,但如果调用了 e.preventDefault() 会导致 pointermove 无法触发 eventFix.pointerdown = { bind: "mousedown", touch: "touchstart", add: pointerAdd, remove: pointerRemove } // CH: 虽然 Chrome 支持 pointer 事件,但不支持 e.preventDefault(),改用 touch+mouse eventFix.pointermove = { bind: "mousemove", touch: "touchmove", add: pointerAdd, remove: pointerRemove } eventFix.pointerup = { bind: "mouseup", touch: "touchend", add(elem: Element | Document, eventHandler: EventListener, eventOptions: any) { pointerAdd.call(this, elem, eventHandler, eventOptions) elem.addEventListener("touchcancel", (eventHandler as any).__touch__, eventOptions) }, remove(elem: Element | Document, eventHandler: EventListener, eventOptions: any) { pointerRemove.call(this, elem, eventHandler, eventOptions) elem.removeEventListener("touchcancel", (eventHandler as any).__touch__, eventOptions) } } } } if (typeof selector === "function") { eventOptions = thisArg thisArg = eventHandler eventHandler = selector selector = "" } thisArg = thisArg || elem if (!eventOptions || !defaultEventOptions) { eventOptions = defaultEventOptions } const events = (elem as any).__events__ || ((elem as any).__events__ = { __proto__: null! }) const key = selector ? eventName + " " + selector : eventName const eventHandlers = events[key] const originalFix = eventFix[eventName] const fix = selector && originalFix && originalFix.delegate ? eventFix[eventName = originalFix.delegate] : originalFix // 如果满足以下任一情况,需要重新封装监听器 // 1. 事件委托,需要重新定位目标元素 // 2. 事件有特殊过滤器,仅在满足条件时触发 // 3. 需要重写回调函数中的 this // 4. 监听器具有第二参数,需要重写回调函数的第二参数 // 5. 监听器已添加函数,需要重新封装才能绑定成功 if (selector || thisArg !== elem || fix && fix.filter || eventHandler.length > 1 || eventHandlers && indexOfHandler(eventHandlers, eventHandler, thisArg) >= 0) { const originalHandler = eventHandler eventHandler = selector ? (e: Event) => { const sender = closest(e.target as Node, selector, elem) if (!sender) { return } if (originalFix && originalFix !== fix && originalFix.filter && originalFix.filter(e, sender) === false) { return } if (fix && fix.filter && fix.filter(e, sender) === false) { return } originalHandler.call(thisArg, e, sender) } : (e: Event) => { if (fix && fix.filter && fix.filter(e, elem) === false) { return } originalHandler.call(thisArg, e, thisArg) } eventHandler.__original__ = originalHandler eventHandler.__this__ = thisArg } // 保存监听器以便之后解绑或手动触发事件 if (!eventHandlers) { events[key] = eventHandler } else if (Array.isArray(eventHandlers)) { eventHandlers.push(eventHandler) } else { events[key] = [eventHandlers, eventHandler] } // 底层绑定事件 if (fix && fix.add) { fix.add(elem, eventHandler, eventOptions) } else { elem.addEventListener(fix && fix.bind || eventName, eventHandler, eventOptions) } } /** * 解绑事件 * @param elem 要处理的元素或文档 * @param eventName 要解绑的事件名 * @param selector 要委托的目标元素的 CSS 选择器 * @param eventHandler 要解绑的事件处理函数如果未提供则解绑所有监听器 * @param thisArg 执行事件处理函数时 this 的值 * @param eventOptions 事件附加选项 * @example off(document.body, "mouseenter", "a", function(e) { this.firstChild.innerHTML = e.pageX; }) */ export function off(elem: Element | Document, eventName: string, selector: string, eventHandler?: (e: Event, sender: HTMLElement) => void, thisArg?: any, eventOptions?: AddEventListenerOptions): void /** * 解绑事件 * @param elem 要处理的元素或文档 * @param eventName 要解绑的事件名 * @param eventHandler 要解绑的事件处理函数如果未提供则解绑所有监听器 * @param thisArg 执行事件处理函数时 this 的值 * @param eventOptions 事件附加选项 * @example off(document.body, "click", e => { alert("点击事件") }) */ export function off(elem: Element | Document, eventName: string, eventHandler?: (e: Event, sender: any) => void, thisArg?: any, eventOptions?: AddEventListenerOptions): void export function off(elem: Element | Document, eventName: string, selector?: string | typeof eventHandler, eventHandler?: (e: Event, sender: any) => void, thisArg?: any, eventOptions?: any) { if (typeof selector === "function") { eventOptions = thisArg thisArg = eventHandler eventHandler = selector selector = "" } thisArg = thisArg || elem if (!eventOptions || !defaultEventOptions) { eventOptions = defaultEventOptions } const events = (elem as any).__events__ const key = selector ? eventName + " " + selector : eventName const eventHandlers = events && events[key] if (!eventHandlers) { return } if (eventHandler) { // 更新事件列表 const index = indexOfHandler(eventHandlers, eventHandler, thisArg) if (~index) { if (Array.isArray(eventHandlers)) { eventHandler = eventHandlers[index] eventHandlers.splice(index, 1) if (!eventHandlers.length) { delete events[key] } } else { eventHandler = eventHandlers delete events[key] } } // 底层解绑事件 const fix = eventFix && eventFix[eventName] as EventFix if (fix && fix.remove) { fix.remove(elem, eventHandler as EventListener, eventOptions) } else { elem.removeEventListener(fix && (selector ? fix.delegate : fix.bind) || eventName, eventHandler as EventListener, eventOptions) } } else if (Array.isArray(eventHandlers)) { for (eventHandler of eventHandlers) { off(elem, eventName, selector!, eventHandler, thisArg) } } else { off(elem, eventName, selector!, eventHandlers, thisArg) } } function indexOfHandler(eventHandlers: typeof eventHandler[] | typeof eventHandler | undefined, eventHandler: (e: Event, sender: Element | Document) => void, thisArg: any) { if (Array.isArray(eventHandlers)) { for (let i = 0; i < eventHandlers.length; i++) { if (eventHandlers[i] === eventHandler || (eventHandlers[i] as any).__original__ === eventHandler && (eventHandlers[i] as any).__this__ === thisArg) { return i } } return -1 } return eventHandlers === eventHandler || (eventHandlers as any).__original__ === eventHandler && (eventHandlers as any).__this__ === thisArg ? 0 : -1 } /** * 触发事件执行已绑定的所有事件处理函数 * @param elem 要处理的元素或文档 * @param eventName 要触发的事件名 * @param selector 要委托的目标元素的 CSS 选择器 * @param event 传递给监听器的事件参数 * @example trigger(document.body, "click") */ export function trigger(elem: Element | Document, eventName: string, selector: string, event?: Partial): void /** * 触发事件执行已绑定的所有事件处理函数 * @param elem 要处理的元素或文档 * @param eventName 要触发的事件名 * @param event 传递给监听器的事件参数 * @example trigger(document.body, "click") */ export function trigger(elem: Element | Document, eventName: string, event?: Partial): void export function trigger(elem: Element | Document, eventName: string, selector: string | typeof event, event?: Partial) { if (typeof selector !== "string") { event = selector selector = "" } const eventHandlers = (elem as any).__events__ && (elem as any).__events__[selector ? eventName + " " + selector : eventName] if (!eventHandlers) { return } event = event || {} if (!event.type) (event as any).type = eventName if (!event.target) (event as any).target = selector ? find(elem, selector) : elem if (Array.isArray(eventHandlers)) { for (const eventHandler of eventHandlers.slice(0)) { eventHandler.call(elem, event) } } else { eventHandlers.call(elem, event) } } /** 存储特效相关配置 */ var animateFix: { /** 是否支持 CSS3 动画 */ support: boolean, /** 当前浏览器实际使用的 transition 属性名 */ transition: string, /** 当前浏览器实际使用的 transitionEnd 事件名 */ transitionEnd: string } /** * 执行一个自定义渐变 * @param elem 要处理的元素 * @param propNames 要渐变的 CSS 属性名和最终的属性值组成的键值对 * @param callback 渐变执行结束的回调函数 * @param duration 渐变执行的总毫秒数 * @param timingFunction 渐变函数可以使用 CSS3 预设的特效渐变函数 * @example animate(document.body, { height: 400 }) */ export function animate(elem: Element & ElementCSSInlineStyle, propNames: { [propName: string]: string | number }, callback?: () => void, duration = 200, timingFunction = "ease") { if (!animateFix) { const transition = vendor("transition") animateFix = { support: transition in document.documentElement!.style, transition: transition, transitionEnd: (transition + "End").replace(transition.length > 10 ? /^[A-Z]/ : /[A-Z]/, w => w.toLowerCase()) } } if (animateFix.support && duration !== 0) { const context = (elem.style as any).__animate__ || ((elem.style as any).__animate__ = {}) const updateTransition = () => { let transition = "" for (const key in context) { if (transition) transition += "," transition += `${key.replace(/[A-Z]|^ms|^webkit/g, word => "-" + word.toLowerCase())} ${duration}ms ${timingFunction}` } elem.style[animateFix.transition as any] = transition } const transitionEnd = (e: Event) => { // 忽略冒泡导致的调用 if (timer && (!e || e.target === e.currentTarget)) { clearTimeout(timer) timer = 0 elem.removeEventListener(animateFix.transitionEnd, transitionEnd, false) // 如果新的渐变覆盖了当前渐变的所有属性,则不触发本次渐变的回调函数 let contextUpdated = false for (const key in context) { if (context[key] === transitionEnd) { delete context[key] contextUpdated = true } } if (contextUpdated) { updateTransition() callback && callback() } } } // 设置所有属性为起始值 for (let propName in propNames) { propName = vendor(propName) context[propName] = transitionEnd if (!elem.style[propName as any]) { elem.style[propName as any] = getStyle(elem, propName) } } // 触发重新布局以保证效果可以触发 (elem as HTMLElement).offsetWidth && elem.clientLeft // 设置要渐变的属性 updateTransition() // 绑定渐变完成事件 elem.addEventListener(animateFix.transitionEnd, transitionEnd, false) let timer = setTimeout(transitionEnd, duration) as any } else { callback && setTimeout(callback, duration) } // 设置属性为最终值,触发动画 for (const propName in propNames) { setStyle(elem, propName, propNames[propName]) } } /** * 判断指定的元素是否被隐藏或正在被隐藏 * @param elem 要处理的元素 * @example isHidden(document.body) */ export function isHidden(elem: Element & ElementCSSInlineStyle) { return (elem.style as any).__toggle__ === false || (elem.style.display || getStyle(elem, "display")) === "none" } /** 存储标签默认的 display 属性 */ var defaultDisplays: { [tagName: string]: string } /** 存储内置切换动画 */ var toggleAnimations: { [animation: string]: { [propName: string]: string | number } } /** 表示一个切换动画 */ export type ToggleAnimation = "opacity" | "height" | "width" | "top" | "bottom" | "left" | "right" | "scale" | "scaleX" | "scaleY" | "slideDown" | "slideRight" | "slideUp" | "slideLeft" | "zoomIn" | "zoomOut" | "rotate" | typeof toggleAnimations[""] /** * 显示一个元素 * @param elem 要处理的元素 * @param animation 显示时使用的动画 * @param callback 动画执行完成后的回调 * @param duration 动画执行的总毫秒数 * @param timingFunction 渐变函数可以使用 CSS3 预设的特效渐变函数 * @param target 动画的目标元素 */ export function show(elem: Element & ElementCSSInlineStyle, animation?: ToggleAnimation, callback?: (value: boolean) => void, duration?: number, timingFunction?: string, target?: Element) { if (animation || callback) { toggle(elem, true, animation, callback, duration, timingFunction, target) } else { elem.style.display = (elem.style as any).__display__ || "" if ((elem.style as any).__toggle__ === false) { delete (elem.style as any).__toggle__ } // 如果清空内联 display 后 display 仍然为 none, 说明通过 CSS 设置了 display 属性 // 这时将元素强制设为 inline 或 block if (getStyle(elem, "display") === "none") { const nodeName = elem.nodeName let defaultDisplay = (defaultDisplays || (defaultDisplays = { __proto__: null! }))[nodeName] if (!defaultDisplay) { // 创建一个新节点以计算其默认的 display 属性 const tmp = document.createElement(nodeName) document.body.appendChild(tmp) defaultDisplay = getStyle(tmp, "display") document.body.removeChild(tmp) // 如果计算失败则设置为默认的 block if (defaultDisplay === "none") { defaultDisplay = "block" } // 缓存以加速下次计算 defaultDisplays[nodeName] = defaultDisplay } elem.style.display = defaultDisplay } } } /** * 隐藏元素 * @param elem 要处理的元素 * @param animation 显示时使用的动画 * @param callback 动画执行完成后的回调 * @param duration 动画执行的总毫秒数 * @param timingFunction 渐变函数可以使用 CSS3 预设的特效渐变函数 * @param target 动画的目标元素 */ export function hide(elem: Element & ElementCSSInlineStyle, animation?: ToggleAnimation, callback?: (value: boolean) => void, duration?: number, timingFunction?: string, target?: Element) { if (animation || callback) { toggle(elem, false, animation, callback, duration, timingFunction, target) } else { const currentDisplay = getStyle(elem, "display") if ((elem.style as any).__toggle__ === true) { delete (elem.style as any).__toggle__ } if (currentDisplay !== "none") { (elem.style as any).__display__ = elem.style.display elem.style.display = "none" } } } /** * 切换显示或隐藏元素 * @param elem 要处理的元素 * @param animation 显示或隐藏时使用的动画 * @param callback 动画执行完成后的回调 * @param duration 动画执行的总毫秒数 * @param timingFunction 渐变函数可以使用 CSS3 预设的特效渐变函数 * @param target 动画的目标元素 * @example * // 折叠/展开 * toggle(document.body, "height") * * // 深入/淡出 * toggle(document.body, "opacity") * * // 缩小/放大 * toggle(document.body, "scale") */ export function toggle(elem: Element & ElementCSSInlineStyle, animation?: ToggleAnimation, callback?: (value: boolean) => void, duration?: number, timingFunction?: string, target?: Element): void /** * 切换显示或隐藏元素 * @param elem 要处理的元素 * @param value 如果为 true 则强制显示元素;如果为 false 则强制隐藏元素 * @param animation 显示或隐藏时使用的动画 * @param callback 动画执行完成后的回调 * @param duration 动画执行的总毫秒数 * @param timingFunction 渐变函数可以使用 CSS3 预设的特效渐变函数 * @param target 动画的目标元素 */ export function toggle(elem: Element & ElementCSSInlineStyle, value: boolean, animation?: ToggleAnimation, callback?: (value: boolean) => void, duration?: number, timingFunction?: string, target?: Element): void export function toggle(elem: Element & ElementCSSInlineStyle, value?: boolean | typeof animation, animation?: ToggleAnimation | typeof callback, callback?: ((value: boolean) => void) | typeof duration, duration?: number | typeof timingFunction, timingFunction?: string | typeof target, target?: Element) { if (typeof value !== "boolean") { target = timingFunction as typeof target timingFunction = duration as string duration = callback as number callback = animation as (this: Element, value: boolean) => void animation = value as ToggleAnimation value = undefined } if (value === undefined) { value = isHidden(elem) } if (typeof animation === "string") { animation = (toggleAnimations || (toggleAnimations = { opacity: { opacity: 0 }, height: { marginTop: 0, borderTopWidth: 0, paddingTop: 0, height: 0, paddingBottom: 0, borderBottomWidth: 0, marginBottom: 0 }, width: { marginLeft: 0, borderLeftWidth: 0, paddingLeft: 0, width: 0, paddinRight: 0, borderRightWidth: 0, marginRight: 0 }, top: { transform: "translateY(-100%)" }, bottom: { transform: "translateY(100%)" }, left: { transform: "translateX(-100%)" }, right: { transform: "translateX(100%)" }, scale: { transform: "scale(0, 0)" }, scaleX: { transform: "scaleX(0)" }, scaleY: { transform: "scaleY(0)" }, slideUp: { opacity: 0, transform: "translateY(10%)" }, slideLeft: { opacity: 0, transform: "translateX(10%)" }, slideDown: { opacity: 0, transform: "translateY(-10%)" }, slideRight: { opacity: 0, transform: "translateX(-10%)" }, zoomOut: { opacity: 0, transform: "scale(0, 0)" }, zoomIn: { opacity: 0, transform: "scale(1.2, 1.2)" }, rotate: { opacity: 0, transform: "rotate(-180deg)" } }))[animation] } if (animation && duration !== 0) { // 优先显示元素以便后续计算 if (value) { show(elem) } // 设置渐变目标 // 如果正在执行渐变,计算新目标会出现错误,直接复用上次设置的目标 const setTransformOrigin = target && (animation as typeof toggleAnimations[""]).transform && (elem.style as any).__toggle__ == undefined if (setTransformOrigin) { const targetRect = getRect(target!) const elemRect = getRect(elem) setStyle(elem, "transformOrigin", `${(elemRect.x + elemRect.width <= targetRect.x + targetRect.width / 4 ? targetRect.x : targetRect.x + targetRect.width <= elemRect.x + targetRect.width / 4 ? targetRect.x + targetRect.width : targetRect.x + targetRect.width / 2) - elemRect.x}px ${(elemRect.y + elemRect.height <= targetRect.y + targetRect.height / 4 ? targetRect.y : targetRect.y + targetRect.height <= elemRect.y + targetRect.height / 4 ? targetRect.y + targetRect.height : targetRect.y + targetRect.height / 2) - elemRect.y}px`) } // 更改宽高时隐藏滚动条 const setOverflowX = (animation as typeof toggleAnimations[""]).width != undefined if (setOverflowX) { elem.style.overflowX = "hidden" } const setOverflowY = (animation as typeof toggleAnimations[""]).height != undefined if (setOverflowY) { elem.style.overflowY = "hidden" } // 计算渐变的最终属性 // 如果需要隐藏元素,则 animation 表示最终属性 // 如果需要显示元素,则需要手动计算最终属性 let to = animation as typeof toggleAnimations[""] if (value) { to = {} // 如果正在执行渐变,则从当前位置开始渐变而非从隐藏时的值属性开始,同时停止渐变用于计算最终属性 let from = animation as typeof toggleAnimations[""] if ((elem.style as any).__toggle__ != undefined) { from = {} for (const prop in animation as typeof toggleAnimations[""]) { from[prop] = getStyle(elem, prop) setStyle(elem, prop, "") } elem.style[animateFix.transition as any] = "" } // 计算最终属性值并将属性重置为初始值 for (const prop in animation as typeof toggleAnimations[""]) { to[prop] = getStyle(elem, prop) setStyle(elem, prop, from[prop]) } } // 执行渐变 (elem.style as any).__toggle__ = value animate(elem, to, () => { delete (elem.style as any).__toggle__ if (setOverflowX) { elem.style.minWidth = elem.style.overflowX = "" } if (setOverflowY) { elem.style.minHeight = elem.style.overflowY = "" } if (setTransformOrigin) { setStyle(elem, "transformOrigin", "") } for (const prop in to) { setStyle(elem, prop, "") } if (!value) { hide(elem) } callback && (callback as Function)(value) }, duration as number, timingFunction as string) } else { value ? show(elem) : hide(elem) callback && (callback as Function)(value) } } /** * 确保在文档加载完成后再执行指定的函数 * @param callback 要执行的回调函数 * @param context 要等待的文档对象 */ export function ready(callback: (this: Document) => void, context = document) { if (/^(?:complete|loaded|interactive)$/.test(context.readyState) && context.body) { callback.call(context) } else { context.addEventListener("DOMContentLoaded", callback, false) } }