import { ElementRef, Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class DomHelperService { constructor() {} // from: https://stackoverflow.com/questions/40597658/equivalent-of-angular-equals-in-angular2#44649659 deepEquals(x: any, y: any) { if (x === y) { return true; // if both x and y are null or undefined and exactly the same } else if (!(x instanceof Object) || !(y instanceof Object)) { return false; // if they are not strictly equal, they both need to be Objects } else if (x.constructor !== y.constructor) { // they must have the exact same prototype chain, the closest we can do is // test their constructor. return false; } else { for (const p in x) { if (!x.hasOwnProperty(p)) { continue; // other properties were tested using x.constructor === y.constructor } if (!y.hasOwnProperty(p)) { return false; // allows to compare x[ p ] and y[ p ] when set to undefined } if (x[p] === y[p]) { continue; // if they have the same strict value or identity then they are equal } if (typeof (x[p]) !== 'object') { return false; // Numbers, Strings, Functions, Booleans must be strictly equal } if (!this.deepEquals(x[p], y[p])) { return false; } } for (const p in y) { if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) { return false; } } return true; } } isHidden(node: ElementRef){ const el = node.nativeElement?node.nativeElement:node; const elemStyle = window.getComputedStyle(el); return el.style.display === 'none' || elemStyle.visibility === 'hidden' || el.hasAttribute('hidden') || elemStyle.display === 'none'; } isTabable(node: ElementRef): boolean { const el = node.nativeElement?node.nativeElement:node; const tagName = el.tagName; if(this.isHidden(node)){ return false; } // el.attribute:NamdedNodeMap if (el.attributes.hasOwnProperty('tabindex')) { return (parseInt(el.attributes.getNamedItem('tabindex'),10) >= 0); } if (tagName === 'A' || tagName === 'AREA' || tagName === 'BUTTON' || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') { if (tagName === 'A' || tagName === 'AREA') { return (el.attributes.getNamedItem('href') !== ''); } return !el.attributes.hasOwnProperty('disabled'); // check for cases when: disabled="true" and disabled="false" } return false; } private isValidChild(child: any): boolean { // child:ElementRef.nativeElement return child.nodeType == 1 && child.nodeName != 'SCRIPT' && child.nodeName != 'STYLE'; } private hasValidParent(obj: any) { // obj:ElementRef.nativeElement return (this.isValidChild(obj) && obj.parentElement.nodeName !== 'BODY'); } private traverse(obj: any, fromTop: boolean): ElementRef | undefined | boolean { // obj:ElementRef||ElementRef.nativeElement var obj = obj? (obj.nativeElement?obj.nativeElement:obj) : document.getElementsByTagName('body')[0]; if (this.isValidChild(obj) && this.isTabable(obj)) { return obj; } // If object is hidden, skip it's children if (this.isValidChild(obj) && this.isHidden(obj)) { return undefined; } // If object is hidden, skip it's children if (obj.classList && obj.classList.contains('ng-hide')) { // some nodes don't have classList?! return false; } if (obj.hasChildNodes()) { var child; if (fromTop) { child = obj.firstChild; } else { child = obj.lastChild; } while(child) { var res = this.traverse(child, fromTop); if(res){ return res; } else{ if (fromTop) { child = child.nextSibling; } else { child = child.previousSibling; } } } } else{ return undefined; } } previousElement(el: any, isFocusable: boolean): any { // ElementRef | undefined | boolean var elem = el.nativeElement ? el.nativeElement : el; if (el.hasOwnProperty('length')) { elem = el[0]; } var parent = elem.parentElement; var previousElem = undefined; if(isFocusable) { if (this.hasValidParent(elem)) { var siblings = parent.children; if (siblings.length > 0) { // Good practice to splice out the elem from siblings if there, saving some time. // We allow for a quick check for jumping to parent first before removing. if (siblings[0] === elem) { // If we are looking at immidiate parent and elem is first child, we need to go higher var e = this.previousElement(elem.parentNode, isFocusable); if (this.isTabable(e)) { return e; } } else { // I need to filter myself and any nodes next to me from the siblings var indexOfElem = Array.prototype.indexOf.call(siblings, elem); const that = this; siblings = Array.prototype.filter.call(siblings, function(item, itemIndex) { if (!that.deepEquals(elem, item) && itemIndex < indexOfElem) { return true; } }); } // We need to search backwards for (var i = 0; i <= siblings.length-1; i++) {//for (var i = siblings.length-1; i >= 0; i--) { var ret = this.traverse(siblings[i], false); if (ret !== undefined) { return ret; } } var e = this.previousElement(elem.parentNode, isFocusable); if (this.isTabable(e)) { return e; } } } } else { var siblings = parent.children; if (siblings.length > 1) { // Since indexOf is on Array.prototype and parent.children is a NodeList, we have to use call() var index = Array.prototype.indexOf.call(siblings, elem); previousElem = siblings[index-1]; } } return previousElem; }; lastTabableElement(el: any) { /* This will return the first tabable element from the parent el */ var elem = el.nativeElement?el.nativeElement:el; if (el.hasOwnProperty('length')) { elem = el[0]; } return this.traverse(elem, false); }; firstTabableElement(el: any) { /* This will return the first tabable element from the parent el */ var elem = el.nativeElement ? el.nativeElement : el; if (el.hasOwnProperty('length')) { elem = el[0]; } return this.traverse(elem, true); }; isInDOM(obj: Node) { return document.documentElement.contains(obj); } }