|
@@ -1,438 +1,444 @@
|
|
|
-function mapTags(tag) {
|
|
|
- let ret = "";
|
|
|
- switch (tag) {
|
|
|
- case "A":
|
|
|
- ret = "Link";
|
|
|
- break;
|
|
|
- case "BUTTON":
|
|
|
- ret = "Button";
|
|
|
- break;
|
|
|
- case "IMG":
|
|
|
- ret = "Image";
|
|
|
- break;
|
|
|
- case "INPUT":
|
|
|
- ret = "Textbox";
|
|
|
- break;
|
|
|
- case "TEXTAREA":
|
|
|
- ret = "Textbox";
|
|
|
- break;
|
|
|
- default:
|
|
|
- ret = "";
|
|
|
- // ret = 'Text'
|
|
|
- break;
|
|
|
- }
|
|
|
- return ret;
|
|
|
-}
|
|
|
-
|
|
|
-function extractTextForFocus(e) {
|
|
|
- let meaningfulNode = e.target;
|
|
|
-
|
|
|
- // 如果天然能focus,但没有被加上tabindex属性,比如focus到了第三方组件内部的可focus元素,直接返回。
|
|
|
- if (
|
|
|
- ["A", "AREA", "BUTTON", "INPUT", "SELECT", "IFRAME"].includes(
|
|
|
- meaningfulNode.tagName
|
|
|
- ) &&
|
|
|
- !meaningfulNode.getAttribute("tabindex")
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- while (
|
|
|
- !meaningfulNode.getAttribute ||
|
|
|
- !meaningfulNode.getAttribute("tabindex")
|
|
|
- ) {
|
|
|
- meaningfulNode = meaningfulNode.parentNode;
|
|
|
- if (!meaningfulNode) {
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 约定:tabindex属性值为-1的元素只用于在点击有data-aria-xxx-area attribute的区域包裹元素的子孙元素时,避免focus到区域包裹元素。
|
|
|
- if (meaningfulNode.getAttribute("tabindex") === "-1") {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let elemType = "";
|
|
|
- const ariaLabel = meaningfulNode.getAttribute("aria-label");
|
|
|
- if (ariaLabel !== null) {
|
|
|
- elemType = ariaLabel;
|
|
|
- } else {
|
|
|
- elemType = mapTags(meaningfulNode.tagName);
|
|
|
- }
|
|
|
-
|
|
|
- let elemDisc = "";
|
|
|
- const ariaDescription = meaningfulNode.getAttribute("aria-description");
|
|
|
- if (ariaDescription !== null) {
|
|
|
- elemDisc = ariaDescription;
|
|
|
- } else {
|
|
|
- elemDisc = meaningfulNode.innerText;
|
|
|
- }
|
|
|
-
|
|
|
- let elemAudioId = "";
|
|
|
- const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
|
|
|
- if (ariaElemAudioId !== null) {
|
|
|
- elemAudioId = ariaElemAudioId;
|
|
|
- } else {
|
|
|
- let ariaElemAudioId2 =
|
|
|
- meaningfulNode.firstElementChild &&
|
|
|
- meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
|
|
|
- if (ariaElemAudioId2) {
|
|
|
- elemAudioId = ariaElemAudioId2;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- elemType,
|
|
|
- elemDisc,
|
|
|
- ariaNode: meaningfulNode,
|
|
|
- elemAudioId,
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-let lastMeaningfulNode = null;
|
|
|
-function extractTextForMouseOver(e) {
|
|
|
- let meaningfulNode = e.target;
|
|
|
-
|
|
|
- while (
|
|
|
- !meaningfulNode.getAttribute ||
|
|
|
- !meaningfulNode.getAttribute("tabindex")
|
|
|
- ) {
|
|
|
- meaningfulNode = meaningfulNode.parentNode;
|
|
|
- if (!meaningfulNode) {
|
|
|
- lastMeaningfulNode = null;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (meaningfulNode.getAttribute("tabindex") === "-1") {
|
|
|
- lastMeaningfulNode = null;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // mouseover事件冒泡到有data-aria-xxx-area attribute的区域包裹元素时,不应该提取该区域包裹元素的无障碍辅助信息。
|
|
|
- if (
|
|
|
- meaningfulNode.getAttribute("data-aria-navigation-area") !== null ||
|
|
|
- meaningfulNode.getAttribute("data-aria-viewport-area") !== null ||
|
|
|
- meaningfulNode.getAttribute("data-aria-interaction-area") !== null
|
|
|
- ) {
|
|
|
- lastMeaningfulNode = null;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果只是在需要朗读的子元素之间进进出出,第一次需要朗读,以后就不需要朗读了。
|
|
|
- let relatedMeaningfulNode = e.relatedTarget;
|
|
|
- while (
|
|
|
- relatedMeaningfulNode &&
|
|
|
- (!relatedMeaningfulNode.getAttribute ||
|
|
|
- !relatedMeaningfulNode.getAttribute("tabindex"))
|
|
|
- ) {
|
|
|
- relatedMeaningfulNode = relatedMeaningfulNode.parentNode;
|
|
|
- }
|
|
|
- if (
|
|
|
- relatedMeaningfulNode === meaningfulNode &&
|
|
|
- lastMeaningfulNode === meaningfulNode
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- lastMeaningfulNode = meaningfulNode;
|
|
|
-
|
|
|
- let elemType = "";
|
|
|
- const ariaLabel = meaningfulNode.getAttribute("aria-label");
|
|
|
- if (ariaLabel !== null) {
|
|
|
- elemType = ariaLabel;
|
|
|
- } else {
|
|
|
- elemType = mapTags(meaningfulNode.tagName);
|
|
|
- }
|
|
|
-
|
|
|
- let elemDisc = "";
|
|
|
- const ariaDescription = meaningfulNode.getAttribute("aria-description");
|
|
|
- if (ariaDescription !== null) {
|
|
|
- elemDisc = ariaDescription;
|
|
|
- } else {
|
|
|
- elemDisc = meaningfulNode.innerText;
|
|
|
- }
|
|
|
-
|
|
|
- let elemAudioId = "";
|
|
|
- const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
|
|
|
- if (ariaElemAudioId !== null) {
|
|
|
- elemAudioId = ariaElemAudioId;
|
|
|
- } else {
|
|
|
- let ariaElemAudioId2 =
|
|
|
- meaningfulNode.firstElementChild &&
|
|
|
- meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
|
|
|
- if (ariaElemAudioId2) {
|
|
|
- elemAudioId = ariaElemAudioId2;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- elemType,
|
|
|
- elemDisc,
|
|
|
- ariaNode: meaningfulNode,
|
|
|
- elemAudioId,
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-function isObject(p) {
|
|
|
- return Object.prototype.toString.call(p) === "[object Object]";
|
|
|
-}
|
|
|
-
|
|
|
-// 判断两个对象内容是否相同
|
|
|
-function isSameObject(object1, object2) {
|
|
|
- const keys1 = Object.keys(object1);
|
|
|
- const keys2 = Object.keys(object2);
|
|
|
-
|
|
|
- if (keys1.length !== keys2.length) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- for (let index = 0; index < keys1.length; index++) {
|
|
|
- const val1 = object1[keys1[index]];
|
|
|
- const val2 = object2[keys2[index]];
|
|
|
- const areObjects = isObject(val1) && isObject(val2);
|
|
|
- if (
|
|
|
- (areObjects && !isSameObject(val1, val2)) ||
|
|
|
- (!areObjects && val1 !== val2)
|
|
|
- ) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- return true;
|
|
|
-}
|
|
|
-
|
|
|
-function getAndFocusNextNodeWithCustomAttribute(attriName) {
|
|
|
- const startNode = document.activeElement || document.body;
|
|
|
-
|
|
|
- const treeWalker = document.createTreeWalker(
|
|
|
- document.body,
|
|
|
- NodeFilter.SHOW_ELEMENT
|
|
|
- );
|
|
|
- treeWalker.currentNode = startNode;
|
|
|
-
|
|
|
- let targetNode = null;
|
|
|
-
|
|
|
- // eslint-disable-next-line
|
|
|
- while (true) {
|
|
|
- const nextNode = treeWalker.nextNode();
|
|
|
- if (!nextNode) {
|
|
|
- console.log("往下没找到");
|
|
|
- break;
|
|
|
- }
|
|
|
- if (nextNode.dataset[attriName] !== undefined) {
|
|
|
- console.log("往下找到了");
|
|
|
- targetNode = nextNode;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (!targetNode && startNode !== document.body) {
|
|
|
- treeWalker.currentNode = document.body;
|
|
|
- // eslint-disable-next-line
|
|
|
- while (true) {
|
|
|
- const nextNode = treeWalker.nextNode();
|
|
|
- if (!nextNode) {
|
|
|
- console.log("往上也没找到");
|
|
|
- break;
|
|
|
- }
|
|
|
- if (nextNode.dataset[attriName] !== undefined) {
|
|
|
- console.log("往上找到了");
|
|
|
- targetNode = nextNode;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (targetNode) {
|
|
|
- // 如果重复选中某一区域,需要重新触发focus,所以先blur一下
|
|
|
- if (document.activeElement === targetNode) {
|
|
|
- targetNode.blur();
|
|
|
- }
|
|
|
- targetNode.focus();
|
|
|
-
|
|
|
- // 如果无法focus,就强行focus
|
|
|
- if (document.activeElement !== targetNode) {
|
|
|
- targetNode.setAttribute("tabindex", "0");
|
|
|
- targetNode.focus();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return targetNode;
|
|
|
-}
|
|
|
-
|
|
|
-function __focusNextFocusableNode(treeWalker) {
|
|
|
- // eslint-disable-next-line
|
|
|
- while (true) {
|
|
|
- const nextNode = treeWalker.nextNode();
|
|
|
- if (!nextNode) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (nextNode.focus) {
|
|
|
- nextNode.focus();
|
|
|
- if (document.activeElement === nextNode && nextNode.tabIndex !== -1) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function iterateOnFocusableNode(startNode, focusedNodeHandler) {
|
|
|
- const treeWalker = document.createTreeWalker(
|
|
|
- document.body,
|
|
|
- NodeFilter.SHOW_ELEMENT
|
|
|
- );
|
|
|
- treeWalker.currentNode = startNode;
|
|
|
- treeWalker.currentNode.focus();
|
|
|
- if (document.activeElement === treeWalker.currentNode) {
|
|
|
- // console.log('起始节点可以focus')
|
|
|
- } else {
|
|
|
- // console.log('起始节点不可以focus,focus到下一节点。')
|
|
|
- const ret = __focusNextFocusableNode(treeWalker);
|
|
|
- if (!ret) {
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const iterator = () => {
|
|
|
- focusedNodeHandler(treeWalker.currentNode)
|
|
|
- .then(() => {
|
|
|
- const result = __focusNextFocusableNode(treeWalker);
|
|
|
- if (result) {
|
|
|
- // console.log('遍历到下一个节点!')
|
|
|
- iterator();
|
|
|
- } else {
|
|
|
- // console.log('遍历结束!')
|
|
|
- }
|
|
|
- })
|
|
|
- .catch((e) => {
|
|
|
- // console.log('遍历中止!', e)
|
|
|
- });
|
|
|
- };
|
|
|
- iterator();
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 返回一个自带消抖效果的函数,用res表示。
|
|
|
- *
|
|
|
- * fn: 需要被消抖的函数
|
|
|
- * delay: 消抖时长
|
|
|
- * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
|
|
|
- * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
|
|
|
- */
|
|
|
-function debounce(
|
|
|
- fn,
|
|
|
- delay,
|
|
|
- isImmediateCall = false,
|
|
|
- isRememberLastCall = true
|
|
|
-) {
|
|
|
- console.assert(
|
|
|
- isImmediateCall || isRememberLastCall,
|
|
|
- "isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!"
|
|
|
- );
|
|
|
- let timer = null;
|
|
|
- // 上次调用的时刻
|
|
|
- let lastCallTime = 0;
|
|
|
-
|
|
|
- if (isImmediateCall && !isRememberLastCall) {
|
|
|
- return function (...args) {
|
|
|
- const currentTime = Date.now();
|
|
|
- if (currentTime - lastCallTime >= delay) {
|
|
|
- fn.apply(this, args);
|
|
|
- }
|
|
|
- lastCallTime = currentTime;
|
|
|
- };
|
|
|
- } else if (!isImmediateCall && isRememberLastCall) {
|
|
|
- return function (...args) {
|
|
|
- if (timer) {
|
|
|
- clearTimeout(timer);
|
|
|
- }
|
|
|
- timer = setTimeout(() => {
|
|
|
- fn.apply(this, args);
|
|
|
- }, delay);
|
|
|
- };
|
|
|
- } else if (isImmediateCall && isRememberLastCall) {
|
|
|
- return function (...args) {
|
|
|
- const currentTime = Date.now();
|
|
|
- if (currentTime - lastCallTime >= delay) {
|
|
|
- // 一组操作中的第一次
|
|
|
- fn.apply(this, args);
|
|
|
- lastCallTime = currentTime;
|
|
|
- return;
|
|
|
- } else {
|
|
|
- // 一组中的后续调用
|
|
|
- if (timer) {
|
|
|
- // 在此之前存在中间调用
|
|
|
- lastCallTime = currentTime;
|
|
|
- clearTimeout(timer);
|
|
|
- }
|
|
|
- timer = setTimeout(() => {
|
|
|
- fn.apply(this, args);
|
|
|
- lastCallTime = 0;
|
|
|
- timer = null;
|
|
|
- }, delay);
|
|
|
- }
|
|
|
- };
|
|
|
- } else {
|
|
|
- console.error("不应该执行到这里!");
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-class DebounceScheduler {
|
|
|
- constructor(fn, delay, context, isImmediateCall = false) {
|
|
|
- this.job = fn;
|
|
|
- this.delay = delay;
|
|
|
- this.context = context;
|
|
|
- this.timer = null;
|
|
|
- this.lastCallTime = 0;
|
|
|
- this.isImmediateCall = isImmediateCall;
|
|
|
- }
|
|
|
- planToDo(...args) {
|
|
|
- if (!this.isImmediateCall) {
|
|
|
- if (this.timer) {
|
|
|
- clearTimeout(this.timer);
|
|
|
- this.timer = null;
|
|
|
- }
|
|
|
- this.timer = setTimeout(() => {
|
|
|
- this.job.apply(this.context, args);
|
|
|
- }, this.delay);
|
|
|
- } else {
|
|
|
- const currentTime = Date.now();
|
|
|
- if (currentTime - this.lastCallTime >= this.delay) {
|
|
|
- // 一组操作中的第一次
|
|
|
- this.job.apply(this.context, args);
|
|
|
- this.lastCallTime = currentTime;
|
|
|
- return;
|
|
|
- } else {
|
|
|
- // 一组中的后续调用
|
|
|
- if (this.timer) {
|
|
|
- // 在此之前存在中间调用
|
|
|
- this.lastCallTime = currentTime;
|
|
|
- clearTimeout(this.timer);
|
|
|
- this.timer = null;
|
|
|
- }
|
|
|
- this.timer = setTimeout(() => {
|
|
|
- this.job.apply(this.context, args);
|
|
|
- this.lastCallTime = 0;
|
|
|
- this.timer = null;
|
|
|
- }, this.delay);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- cancel() {
|
|
|
- if (this.timer) {
|
|
|
- clearTimeout(this.timer);
|
|
|
- this.timer = null;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export default {
|
|
|
- mapTags,
|
|
|
- extractTextForFocus,
|
|
|
- extractTextForMouseOver,
|
|
|
- isSameObject,
|
|
|
- getAndFocusNextNodeWithCustomAttribute,
|
|
|
- iterateOnFocusableNode,
|
|
|
- debounce,
|
|
|
- DebounceScheduler,
|
|
|
-};
|
|
|
+function mapTags(tag) {
|
|
|
+ let ret = "";
|
|
|
+ switch (tag) {
|
|
|
+ case "A":
|
|
|
+ ret = "Link";
|
|
|
+ break;
|
|
|
+ case "BUTTON":
|
|
|
+ ret = "Button";
|
|
|
+ break;
|
|
|
+ case "IMG":
|
|
|
+ ret = "Image";
|
|
|
+ break;
|
|
|
+ case "INPUT":
|
|
|
+ ret = "Textbox";
|
|
|
+ break;
|
|
|
+ case "TEXTAREA":
|
|
|
+ ret = "Textbox";
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ ret = "";
|
|
|
+ // ret = 'Text'
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+function extractTextForFocus(e) {
|
|
|
+ let meaningfulNode = e.target;
|
|
|
+
|
|
|
+ // 如果天然能focus,但没有被加上tabindex属性,比如focus到了第三方组件内部的可focus元素,直接返回。
|
|
|
+ if (
|
|
|
+ ["A", "AREA", "BUTTON", "INPUT", "SELECT", "IFRAME"].includes(
|
|
|
+ meaningfulNode.tagName
|
|
|
+ ) &&
|
|
|
+ !meaningfulNode.getAttribute("tabindex")
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ while (
|
|
|
+ !meaningfulNode.getAttribute ||
|
|
|
+ !meaningfulNode.getAttribute("tabindex")
|
|
|
+ ) {
|
|
|
+ meaningfulNode = meaningfulNode.parentNode;
|
|
|
+ if (!meaningfulNode) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 约定:tabindex属性值为-1的元素只用于在点击有data-aria-xxx-area attribute的区域包裹元素的子孙元素时,避免focus到区域包裹元素。
|
|
|
+ if (meaningfulNode.getAttribute("tabindex") === "-1") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let elemType = "";
|
|
|
+ const ariaLabel = meaningfulNode.getAttribute("aria-label");
|
|
|
+ if (ariaLabel !== null) {
|
|
|
+ elemType = ariaLabel;
|
|
|
+ } else {
|
|
|
+ elemType = mapTags(meaningfulNode.tagName);
|
|
|
+ }
|
|
|
+
|
|
|
+ let elemDisc = "";
|
|
|
+ const ariaDescription = meaningfulNode.getAttribute("aria-description");
|
|
|
+ if (ariaDescription !== null) {
|
|
|
+ elemDisc = ariaDescription;
|
|
|
+ } else {
|
|
|
+ elemDisc = meaningfulNode.innerText;
|
|
|
+ }
|
|
|
+
|
|
|
+ let elemAudioId = "";
|
|
|
+ const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
|
|
|
+ if (ariaElemAudioId !== null) {
|
|
|
+ elemAudioId = ariaElemAudioId;
|
|
|
+ } else {
|
|
|
+ let ariaElemAudioId2 =
|
|
|
+ meaningfulNode.firstElementChild &&
|
|
|
+ meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
|
|
|
+ if (ariaElemAudioId2) {
|
|
|
+ elemAudioId = ariaElemAudioId2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let eleAudioUrl = meaningfulNode.getAttribute("aria-audio-url");
|
|
|
+
|
|
|
+ return {
|
|
|
+ elemType,
|
|
|
+ elemDisc,
|
|
|
+ ariaNode: meaningfulNode,
|
|
|
+ elemAudioId,
|
|
|
+ eleAudioUrl,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+let lastMeaningfulNode = null;
|
|
|
+function extractTextForMouseOver(e) {
|
|
|
+ let meaningfulNode = e.target;
|
|
|
+
|
|
|
+ while (
|
|
|
+ !meaningfulNode.getAttribute ||
|
|
|
+ !meaningfulNode.getAttribute("tabindex")
|
|
|
+ ) {
|
|
|
+ meaningfulNode = meaningfulNode.parentNode;
|
|
|
+ if (!meaningfulNode) {
|
|
|
+ lastMeaningfulNode = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (meaningfulNode.getAttribute("tabindex") === "-1") {
|
|
|
+ lastMeaningfulNode = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // mouseover事件冒泡到有data-aria-xxx-area attribute的区域包裹元素时,不应该提取该区域包裹元素的无障碍辅助信息。
|
|
|
+ if (
|
|
|
+ meaningfulNode.getAttribute("data-aria-navigation-area") !== null ||
|
|
|
+ meaningfulNode.getAttribute("data-aria-viewport-area") !== null ||
|
|
|
+ meaningfulNode.getAttribute("data-aria-interaction-area") !== null
|
|
|
+ ) {
|
|
|
+ lastMeaningfulNode = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果只是在需要朗读的子元素之间进进出出,第一次需要朗读,以后就不需要朗读了。
|
|
|
+ let relatedMeaningfulNode = e.relatedTarget;
|
|
|
+ while (
|
|
|
+ relatedMeaningfulNode &&
|
|
|
+ (!relatedMeaningfulNode.getAttribute ||
|
|
|
+ !relatedMeaningfulNode.getAttribute("tabindex"))
|
|
|
+ ) {
|
|
|
+ relatedMeaningfulNode = relatedMeaningfulNode.parentNode;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ relatedMeaningfulNode === meaningfulNode &&
|
|
|
+ lastMeaningfulNode === meaningfulNode
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ lastMeaningfulNode = meaningfulNode;
|
|
|
+
|
|
|
+ let elemType = "";
|
|
|
+ const ariaLabel = meaningfulNode.getAttribute("aria-label");
|
|
|
+ if (ariaLabel !== null) {
|
|
|
+ elemType = ariaLabel;
|
|
|
+ } else {
|
|
|
+ elemType = mapTags(meaningfulNode.tagName);
|
|
|
+ }
|
|
|
+
|
|
|
+ let elemDisc = "";
|
|
|
+ const ariaDescription = meaningfulNode.getAttribute("aria-description");
|
|
|
+ if (ariaDescription !== null) {
|
|
|
+ elemDisc = ariaDescription;
|
|
|
+ } else {
|
|
|
+ elemDisc = meaningfulNode.innerText;
|
|
|
+ }
|
|
|
+
|
|
|
+ let elemAudioId = "";
|
|
|
+ const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
|
|
|
+ if (ariaElemAudioId !== null) {
|
|
|
+ elemAudioId = ariaElemAudioId;
|
|
|
+ } else {
|
|
|
+ let ariaElemAudioId2 =
|
|
|
+ meaningfulNode.firstElementChild &&
|
|
|
+ meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
|
|
|
+ if (ariaElemAudioId2) {
|
|
|
+ elemAudioId = ariaElemAudioId2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let eleAudioUrl = meaningfulNode.getAttribute("aria-audio-url");
|
|
|
+
|
|
|
+ return {
|
|
|
+ elemType,
|
|
|
+ elemDisc,
|
|
|
+ ariaNode: meaningfulNode,
|
|
|
+ elemAudioId,
|
|
|
+ eleAudioUrl,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function isObject(p) {
|
|
|
+ return Object.prototype.toString.call(p) === "[object Object]";
|
|
|
+}
|
|
|
+
|
|
|
+// 判断两个对象内容是否相同
|
|
|
+function isSameObject(object1, object2) {
|
|
|
+ const keys1 = Object.keys(object1);
|
|
|
+ const keys2 = Object.keys(object2);
|
|
|
+
|
|
|
+ if (keys1.length !== keys2.length) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let index = 0; index < keys1.length; index++) {
|
|
|
+ const val1 = object1[keys1[index]];
|
|
|
+ const val2 = object2[keys2[index]];
|
|
|
+ const areObjects = isObject(val1) && isObject(val2);
|
|
|
+ if (
|
|
|
+ (areObjects && !isSameObject(val1, val2)) ||
|
|
|
+ (!areObjects && val1 !== val2)
|
|
|
+ ) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function getAndFocusNextNodeWithCustomAttribute(attriName) {
|
|
|
+ const startNode = document.activeElement || document.body;
|
|
|
+
|
|
|
+ const treeWalker = document.createTreeWalker(
|
|
|
+ document.body,
|
|
|
+ NodeFilter.SHOW_ELEMENT
|
|
|
+ );
|
|
|
+ treeWalker.currentNode = startNode;
|
|
|
+
|
|
|
+ let targetNode = null;
|
|
|
+
|
|
|
+ // eslint-disable-next-line
|
|
|
+ while (true) {
|
|
|
+ const nextNode = treeWalker.nextNode();
|
|
|
+ if (!nextNode) {
|
|
|
+ console.log("往下没找到");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (nextNode.dataset[attriName] !== undefined) {
|
|
|
+ console.log("往下找到了");
|
|
|
+ targetNode = nextNode;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!targetNode && startNode !== document.body) {
|
|
|
+ treeWalker.currentNode = document.body;
|
|
|
+ // eslint-disable-next-line
|
|
|
+ while (true) {
|
|
|
+ const nextNode = treeWalker.nextNode();
|
|
|
+ if (!nextNode) {
|
|
|
+ console.log("往上也没找到");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (nextNode.dataset[attriName] !== undefined) {
|
|
|
+ console.log("往上找到了");
|
|
|
+ targetNode = nextNode;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetNode) {
|
|
|
+ // 如果重复选中某一区域,需要重新触发focus,所以先blur一下
|
|
|
+ if (document.activeElement === targetNode) {
|
|
|
+ targetNode.blur();
|
|
|
+ }
|
|
|
+ targetNode.focus();
|
|
|
+
|
|
|
+ // 如果无法focus,就强行focus
|
|
|
+ if (document.activeElement !== targetNode) {
|
|
|
+ targetNode.setAttribute("tabindex", "0");
|
|
|
+ targetNode.focus();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return targetNode;
|
|
|
+}
|
|
|
+
|
|
|
+function __focusNextFocusableNode(treeWalker) {
|
|
|
+ // eslint-disable-next-line
|
|
|
+ while (true) {
|
|
|
+ const nextNode = treeWalker.nextNode();
|
|
|
+ if (!nextNode) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (nextNode.focus) {
|
|
|
+ nextNode.focus();
|
|
|
+ if (document.activeElement === nextNode && nextNode.tabIndex !== -1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function iterateOnFocusableNode(startNode, focusedNodeHandler) {
|
|
|
+ const treeWalker = document.createTreeWalker(
|
|
|
+ document.body,
|
|
|
+ NodeFilter.SHOW_ELEMENT
|
|
|
+ );
|
|
|
+ treeWalker.currentNode = startNode;
|
|
|
+ treeWalker.currentNode.focus();
|
|
|
+ if (document.activeElement === treeWalker.currentNode) {
|
|
|
+ // console.log('起始节点可以focus')
|
|
|
+ } else {
|
|
|
+ // console.log('起始节点不可以focus,focus到下一节点。')
|
|
|
+ const ret = __focusNextFocusableNode(treeWalker);
|
|
|
+ if (!ret) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const iterator = () => {
|
|
|
+ focusedNodeHandler(treeWalker.currentNode)
|
|
|
+ .then(() => {
|
|
|
+ const result = __focusNextFocusableNode(treeWalker);
|
|
|
+ if (result) {
|
|
|
+ // console.log('遍历到下一个节点!')
|
|
|
+ iterator();
|
|
|
+ } else {
|
|
|
+ // console.log('遍历结束!')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((e) => {
|
|
|
+ // console.log('遍历中止!', e)
|
|
|
+ });
|
|
|
+ };
|
|
|
+ iterator();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 返回一个自带消抖效果的函数,用res表示。
|
|
|
+ *
|
|
|
+ * fn: 需要被消抖的函数
|
|
|
+ * delay: 消抖时长
|
|
|
+ * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
|
|
|
+ * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
|
|
|
+ */
|
|
|
+function debounce(
|
|
|
+ fn,
|
|
|
+ delay,
|
|
|
+ isImmediateCall = false,
|
|
|
+ isRememberLastCall = true
|
|
|
+) {
|
|
|
+ console.assert(
|
|
|
+ isImmediateCall || isRememberLastCall,
|
|
|
+ "isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!"
|
|
|
+ );
|
|
|
+ let timer = null;
|
|
|
+ // 上次调用的时刻
|
|
|
+ let lastCallTime = 0;
|
|
|
+
|
|
|
+ if (isImmediateCall && !isRememberLastCall) {
|
|
|
+ return function (...args) {
|
|
|
+ const currentTime = Date.now();
|
|
|
+ if (currentTime - lastCallTime >= delay) {
|
|
|
+ fn.apply(this, args);
|
|
|
+ }
|
|
|
+ lastCallTime = currentTime;
|
|
|
+ };
|
|
|
+ } else if (!isImmediateCall && isRememberLastCall) {
|
|
|
+ return function (...args) {
|
|
|
+ if (timer) {
|
|
|
+ clearTimeout(timer);
|
|
|
+ }
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ fn.apply(this, args);
|
|
|
+ }, delay);
|
|
|
+ };
|
|
|
+ } else if (isImmediateCall && isRememberLastCall) {
|
|
|
+ return function (...args) {
|
|
|
+ const currentTime = Date.now();
|
|
|
+ if (currentTime - lastCallTime >= delay) {
|
|
|
+ // 一组操作中的第一次
|
|
|
+ fn.apply(this, args);
|
|
|
+ lastCallTime = currentTime;
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ // 一组中的后续调用
|
|
|
+ if (timer) {
|
|
|
+ // 在此之前存在中间调用
|
|
|
+ lastCallTime = currentTime;
|
|
|
+ clearTimeout(timer);
|
|
|
+ }
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ fn.apply(this, args);
|
|
|
+ lastCallTime = 0;
|
|
|
+ timer = null;
|
|
|
+ }, delay);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ console.error("不应该执行到这里!");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class DebounceScheduler {
|
|
|
+ constructor(fn, delay, context, isImmediateCall = false) {
|
|
|
+ this.job = fn;
|
|
|
+ this.delay = delay;
|
|
|
+ this.context = context;
|
|
|
+ this.timer = null;
|
|
|
+ this.lastCallTime = 0;
|
|
|
+ this.isImmediateCall = isImmediateCall;
|
|
|
+ }
|
|
|
+ planToDo(...args) {
|
|
|
+ if (!this.isImmediateCall) {
|
|
|
+ if (this.timer) {
|
|
|
+ clearTimeout(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ }
|
|
|
+ this.timer = setTimeout(() => {
|
|
|
+ this.job.apply(this.context, args);
|
|
|
+ }, this.delay);
|
|
|
+ } else {
|
|
|
+ const currentTime = Date.now();
|
|
|
+ if (currentTime - this.lastCallTime >= this.delay) {
|
|
|
+ // 一组操作中的第一次
|
|
|
+ this.job.apply(this.context, args);
|
|
|
+ this.lastCallTime = currentTime;
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ // 一组中的后续调用
|
|
|
+ if (this.timer) {
|
|
|
+ // 在此之前存在中间调用
|
|
|
+ this.lastCallTime = currentTime;
|
|
|
+ clearTimeout(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ }
|
|
|
+ this.timer = setTimeout(() => {
|
|
|
+ this.job.apply(this.context, args);
|
|
|
+ this.lastCallTime = 0;
|
|
|
+ this.timer = null;
|
|
|
+ }, this.delay);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ cancel() {
|
|
|
+ if (this.timer) {
|
|
|
+ clearTimeout(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ mapTags,
|
|
|
+ extractTextForFocus,
|
|
|
+ extractTextForMouseOver,
|
|
|
+ isSameObject,
|
|
|
+ getAndFocusNextNodeWithCustomAttribute,
|
|
|
+ iterateOnFocusableNode,
|
|
|
+ debounce,
|
|
|
+ DebounceScheduler,
|
|
|
+};
|