frameworks/base/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts

298 lines
9.4 KiB
TypeScript
Raw Permalink Normal View History

2025-08-25 08:17:13 +08:00
// Copyright 2022 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
type IElementComment =
| { commentNode: undefined; textContent: undefined; hidden: undefined }
| { commentNode: Node; textContent: string; hidden: boolean };
interface ITag {
attrs?: Record<string, string | number>;
tagName: string;
}
export interface INewTag extends ITag {
content?: string | number;
comment?: string;
}
export type IUpdateTag = Partial<Omit<INewTag, 'tagName'>>;
export default class DOM {
static addEntry(containerElement: Element, tagOptions: INewTag) {
const doc = containerElement.ownerDocument;
const exists = this.alreadyHasEntry(containerElement, tagOptions);
if (exists) {
console.log('Ignored adding entry already available: ', exists.outerHTML);
return;
}
let insertPoint: Node | null = containerElement.lastElementChild; //.childNodes[containerElement.childNodes.length - 1];
if (!insertPoint) {
console.log('Ignored adding entry in empity parent: ', containerElement.outerHTML);
return;
}
const { attrs, comment, content, tagName } = tagOptions;
if (comment) {
const commentNode = doc.createComment(comment);
this.insertAfterIdented(commentNode, insertPoint);
insertPoint = commentNode;
}
const newEl = doc.createElement(tagName);
if (content) newEl.innerHTML = content.toString();
if (attrs)
Object.entries(attrs).forEach(([attr, value]) =>
newEl.setAttribute(attr, value.toString())
);
this.insertAfterIdented(newEl, insertPoint);
return true;
}
static insertBeforeIndented(newNode: Node, referenceNode: Node) {
const paddingNode = referenceNode.previousSibling;
const ownerDoc = referenceNode.ownerDocument;
const containerNode = referenceNode.parentNode;
if (!paddingNode || !ownerDoc || !containerNode) return;
const currentPadding = paddingNode.textContent || '';
const textNode = referenceNode.ownerDocument.createTextNode(currentPadding);
containerNode.insertBefore(newNode, referenceNode);
containerNode.insertBefore(textNode, newNode);
}
static insertAfterIdented(newNode: Node, referenceNode: Node) {
const paddingNode = referenceNode.previousSibling;
const ownerDoc = referenceNode.ownerDocument;
const containerNode = referenceNode.parentNode;
if (!paddingNode || !ownerDoc || !containerNode) return;
const currentPadding = paddingNode.textContent || '';
const textNode = ownerDoc.createTextNode(currentPadding);
containerNode.insertBefore(newNode, referenceNode.nextSibling);
containerNode.insertBefore(textNode, newNode);
}
static getElementComment(el: Element): IElementComment {
const commentNode = el.previousSibling?.previousSibling;
const out = { commentNode: undefined, textContent: undefined, hidden: undefined };
if (!commentNode) return out;
const textContent = commentNode.textContent || '';
const hidden = textContent.substring(textContent.length - 6) == '@hide ';
if (!(commentNode && commentNode.nodeName == '#comment')) return out;
return { commentNode, textContent, hidden: hidden };
}
static duplicateEntryWithChange(
templateElement: Element,
options: Omit<IUpdateTag, 'content'>
) {
const exists = this.futureEntryAlreadyExist(templateElement, options);
if (exists) {
console.log('Ignored duplicating entry already available: ', exists.outerHTML);
return;
}
const { commentNode } = this.getElementComment(templateElement);
let insertPoint: Node = templateElement;
if (commentNode) {
const newComment = commentNode.cloneNode();
this.insertAfterIdented(newComment, insertPoint);
insertPoint = newComment;
}
const newEl = templateElement.cloneNode(true) as Element;
this.insertAfterIdented(newEl, insertPoint);
this.updateElement(newEl, options);
return true;
}
static replaceStringInAttributeValueOnQueried(
root: Element,
query: string,
attrArray: string[],
replaceMap: Map<string, string>
): boolean {
let updated = false;
const queried = [...Array.from(root.querySelectorAll(query)), root];
queried.forEach((el) => {
attrArray.forEach((attr) => {
if (el.hasAttribute(attr)) {
const currentAttrValue = el.getAttribute(attr);
if (!currentAttrValue) return;
[...replaceMap.entries()].some(([oldStr, newStr]) => {
if (
currentAttrValue.length >= oldStr.length &&
currentAttrValue.indexOf(oldStr) ==
currentAttrValue.length - oldStr.length
) {
el.setAttribute(attr, currentAttrValue.replace(oldStr, newStr));
updated = true;
return true;
}
return false;
});
}
});
});
return updated;
}
static updateElement(el: Element, updateOptions: IUpdateTag) {
const exists = this.futureEntryAlreadyExist(el, updateOptions);
if (exists) {
console.log('Ignored updating entry already available: ', exists.outerHTML);
return;
}
const { comment, attrs, content } = updateOptions;
if (comment) {
const { commentNode } = this.getElementComment(el);
if (commentNode) {
commentNode.textContent = comment;
}
}
if (attrs) {
for (const attr in attrs) {
const value = attrs[attr];
if (value != undefined) {
el.setAttribute(attr, `${value}`);
} else {
el.removeAttribute(attr);
}
}
}
if (content != undefined) {
el.innerHTML = `${content}`;
}
return true;
}
static elementToOptions(el: Element): ITag {
return {
attrs: this.getAllElementAttributes(el),
tagName: el.tagName,
};
}
static getAllElementAttributes(el: Element): Record<string, string> {
return el
.getAttributeNames()
.reduce(
(acc, attr) => ({ ...acc, [attr]: el.getAttribute(attr) || '' }),
{} as Record<string, string>
);
}
static futureEntryAlreadyExist(el: Element, updateOptions: IUpdateTag) {
const currentElOptions = this.elementToOptions(el);
if (!el.parentElement) {
console.log('Checked el has no parent');
process.exit();
}
return this.alreadyHasEntry(el.parentElement, {
...currentElOptions,
...updateOptions,
attrs: { ...currentElOptions.attrs, ...updateOptions.attrs },
});
}
static alreadyHasEntry(
containerElement: Element,
{ attrs, tagName }: Pick<INewTag, 'attrs' | 'tagName'>
) {
const qAttrs = attrs
? Object.entries(attrs)
.map(([a, v]) => `[${a}="${v}"]`)
.join('')
: '';
return containerElement.querySelector(tagName + qAttrs);
}
static replaceContentTextOnQueried(
root: Element,
query: string,
replacePairs: Array<[string, string]>
) {
let updated = false;
let queried = Array.from(root.querySelectorAll(query));
if (queried.length == 0) queried = [...Array.from(root.querySelectorAll(query)), root];
queried.forEach((el) => {
replacePairs.forEach(([oldStr, newStr]) => {
if (el.innerHTML == oldStr) {
el.innerHTML = newStr;
updated = true;
}
});
});
return updated;
}
static XMLDocToString(doc: XMLDocument) {
let str = '';
doc.childNodes.forEach((node) => {
switch (node.nodeType) {
case 8: // comment
str += `<!--${node.nodeValue}-->\n`;
break;
case 3: // text
str += node.textContent;
break;
case 1: // element
str += (node as Element).outerHTML;
break;
default:
console.log('Unhandled node type: ' + node.nodeType);
break;
}
});
return str;
}
}