
# Added - Added: Ability to check for updates (stable-only) and be notified with a changelog. This is a first pass implementation. - Added: Ability to use SignalR within Kavita (websockets) ===================================== * (some debug code present). Implemented the ability to check and log if the server is up to date or not. * Fixed a bug for dark mode where anchor buttons wouldn't have the correct font color. Suppress filter/sort button if there is no filters to show. Debug: Active indicators for users currently on your server. Refactored code to send update notification only to admins. Admins now get a popup where they can open the Github release (docker users can just close). * Fixed an issue where getLibraryNames on first load would call for as many cards there was on the screen. Now we call it much earlier and the data is cached faster. * Fixed a dark mode bug from previous commit * Release notes is now rendered markdown * Implemented the ability to check for an update ad-hoc. Response will come via websocket to all admins. * Fixed a missing padding * Cleanup, added some temp code to carousel * Cleaned up old stat stuff from dev config and added debug only flow for checking for update * Misc cleanup * Added readonly to one variable * Fixed In Progress not showing for all series due to pagination bug * Fixed the In progress API returning back series that had another users progress on them. Added SplitQuery which speeds up query significantly. * SplitQuery in GetRecentlyAdded for a speed increase on API. Fixed the logic on VersionUpdaterService to properly send on non-dev systems. Disable the check button once it's triggered once since the API does a task, so it can't return anything. * Cleaned up the admin actions to be more friendly on mobile. * Cleaned up the message as we wait for SingalR to notify the user * more textual changes * Code smells
200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
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);
|
|
}
|
|
|
|
}
|