* rrdom: add a diff function for properties * implement diffChildren function and unit tests * finish basic functions of diff algorithm * fix several bugs in the diff algorithm * replace the virtual parent optimization in applyMutation() * fix: moveAndHover after the diff algorithm is executed * replace virtual style map with rrdom cssom version has to be above 0.5.0 to pass virtual style tests * fix: failed virtual style tests in replayer.test.ts * fix: failed polyfill tests caused by nodejs compatibility of different versions * fix: svg viewBox attribute doesn't work Cause the attribute viewBox is case sensitive, set value for viewbox doesn't work * feat: replace treeIndex optimization with rrdom * fix bug of diffProps and disable smooth scrolling animation in fast-forward mode * feat: add iframe support * fix: @rollup/plugin-typescript build errors in rrweb-player Error: @rollup/plugin-typescript TS1371: This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValues' is set to 'error' * fix: bug when fast-forward input events and add test for it * add test for fast-forward scroll events * fix: custom style rules don't get inserted into some iframe elements * code style tweak * fix: enable to diff iframe elements * fix the jest error "Unexpected token 'export'" * try to fix build error of rrweb-player * correct the attributes definition in rrdom * fix: custom style rules are not inserted in some iframes * add support for shadow dom * add support for MediaInteraction * add canvas support * fix unit test error in rrdom * add support for Text, Comment * try to refactor RRDom * refactor RRDom to reduce duplicate code * rename document-browser to virtual-dom * increase the test coverage for document.ts and add ownerDocument for it * Merge branch 'master' into virtual-dom * add more test for virtual-dom.ts * use cssstyle in document-nodejs * fix: bundle error * improve document-nodejs * enable to diff scroll positions of an element * rename rrdom to virtualDom for more readability and make the tree public * revert unknown change * improve the css style parser for comments * improve code style * update typings * add handling for the case where legacy_missingNodeRetryMap is not empty * only import types from rrweb into rrdom * Apply suggestions from code review Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> * Apply suggestions from code review * fix building error in rrweb * add a method setDefaultSN to set a default value for a RRNode's __sn * fix rrweb test error and bump up other packages * add support for custom property of css styles * add a switch for virtual-dom optimization * Apply suggestions from code review 1. add an enum type for NodeType 2. rename nodeType from rrweb-snapshot to RRNodeType 3. rename notSerializedId to unserializedId 4. add comments for some confusing variables * adapt changes of #865 to virtual-dom and improve the test case for more coverage * apply review suggestions https://github.com/rrweb-io/rrweb/pull/853#pullrequestreview-922474953 * tweak the diff algorithm * add description of the flag useVirtualDom and remove outdated logConfig * Remove console.log * Contain changes to document * Upgrade rollup to 2.70.2 * Revert "Upgrade rollup to 2.70.2" This reverts commit b1be81a2a76565935c9dc391f31beb7f64d25956. * Fix type checking rrdom * Fix typing error while bundling * Fix tslib error on build Rollup would output the following error: `semantic error TS2343: This syntax requires an imported helper named '__spreadArray' which does not exist in 'tslib'. Consider upgrading your version of 'tslib'.` * Increase memory limit for rollup * Use esbuild for bundling Speeds up bundling significantly * Avoid circular dependencies and import un-bundled rrdom * Fix imports * Revert back to pre-esbuild This reverts the following commits: b7b3c8dbaa551a0129da1477136b1baaad28e6e1 72e23b8e27f9030d911358d3a17fe5ad1b3b5d4f 85d600a20c56cfa764cf1f858932ba14e67b1d23 61e1a5d323212ca8fbe0569e0b3062ddd53fc612 * Set node to lts (12 is no longer supported) * Speed up bundling and use less memory This fixes the out of memory errors happening while bundling * remove __sn from rrdom * fix typo * test: add a test case for StyleSheet mutation exceptions while fast-forwarding * rename Array.prototype.slice.call() to Array.from() * improve test cases * fix: PR #887 in 'virtual-dom' branch * apply justin's suggestion on 'Array.from' refactor related commit 0f6729d27a323260b36fbe79485a86715c0bc98a * improve import code structure Co-authored-by: Yun Feng <yun.feng@anu.edu.au>
This commit is contained in:
723
packages/rrdom/src/document.ts
Normal file
723
packages/rrdom/src/document.ts
Normal file
@@ -0,0 +1,723 @@
|
||||
import { NodeType as RRNodeType } from 'rrweb-snapshot';
|
||||
import { parseCSSText, camelize, toCSSText } from './style';
|
||||
export interface IRRNode {
|
||||
parentElement: IRRNode | null;
|
||||
parentNode: IRRNode | null;
|
||||
childNodes: IRRNode[];
|
||||
ownerDocument: IRRDocument;
|
||||
readonly ELEMENT_NODE: number;
|
||||
readonly TEXT_NODE: number;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
readonly nodeType: number;
|
||||
readonly nodeName: string; // https://dom.spec.whatwg.org/#dom-node-nodename
|
||||
readonly RRNodeType: RRNodeType;
|
||||
|
||||
firstChild: IRRNode | null;
|
||||
|
||||
lastChild: IRRNode | null;
|
||||
|
||||
nextSibling: IRRNode | null;
|
||||
|
||||
textContent: string | null;
|
||||
|
||||
contains(node: IRRNode): boolean;
|
||||
|
||||
appendChild(newChild: IRRNode): IRRNode;
|
||||
|
||||
insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode;
|
||||
|
||||
removeChild(node: IRRNode): IRRNode;
|
||||
|
||||
toString(): string;
|
||||
}
|
||||
export interface IRRDocument extends IRRNode {
|
||||
documentElement: IRRElement | null;
|
||||
|
||||
body: IRRElement | null;
|
||||
|
||||
head: IRRElement | null;
|
||||
|
||||
implementation: IRRDocument;
|
||||
|
||||
firstElementChild: IRRElement | null;
|
||||
|
||||
readonly nodeName: '#document';
|
||||
|
||||
compatMode: 'BackCompat' | 'CSS1Compat';
|
||||
|
||||
createDocument(
|
||||
_namespace: string | null,
|
||||
_qualifiedName: string | null,
|
||||
_doctype?: DocumentType | null,
|
||||
): IRRDocument;
|
||||
|
||||
createDocumentType(
|
||||
qualifiedName: string,
|
||||
publicId: string,
|
||||
systemId: string,
|
||||
): IRRDocumentType;
|
||||
|
||||
createElement(tagName: string): IRRElement;
|
||||
|
||||
createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement;
|
||||
|
||||
createTextNode(data: string): IRRText;
|
||||
|
||||
createComment(data: string): IRRComment;
|
||||
|
||||
createCDATASection(data: string): IRRCDATASection;
|
||||
|
||||
open(): void;
|
||||
|
||||
close(): void;
|
||||
|
||||
write(content: string): void;
|
||||
}
|
||||
export interface IRRElement extends IRRNode {
|
||||
tagName: string;
|
||||
attributes: Record<string, string>;
|
||||
shadowRoot: IRRElement | null;
|
||||
scrollLeft?: number;
|
||||
scrollTop?: number;
|
||||
id: string;
|
||||
className: string;
|
||||
classList: ClassList;
|
||||
style: CSSStyleDeclaration;
|
||||
|
||||
attachShadow(init: ShadowRootInit): IRRElement;
|
||||
|
||||
getAttribute(name: string): string | null;
|
||||
|
||||
setAttribute(name: string, attribute: string): void;
|
||||
|
||||
setAttributeNS(
|
||||
namespace: string | null,
|
||||
qualifiedName: string,
|
||||
value: string,
|
||||
): void;
|
||||
|
||||
removeAttribute(name: string): void;
|
||||
|
||||
dispatchEvent(event: Event): boolean;
|
||||
}
|
||||
export interface IRRDocumentType extends IRRNode {
|
||||
readonly name: string;
|
||||
readonly publicId: string;
|
||||
readonly systemId: string;
|
||||
}
|
||||
export interface IRRText extends IRRNode {
|
||||
readonly nodeName: '#text';
|
||||
data: string;
|
||||
}
|
||||
export interface IRRComment extends IRRNode {
|
||||
readonly nodeName: '#comment';
|
||||
data: string;
|
||||
}
|
||||
export interface IRRCDATASection extends IRRNode {
|
||||
readonly nodeName: '#cdata-section';
|
||||
data: string;
|
||||
}
|
||||
|
||||
type ConstrainedConstructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
/**
|
||||
* This is designed as an abstract class so it should never be instantiated.
|
||||
*/
|
||||
export class BaseRRNode implements IRRNode {
|
||||
public childNodes: IRRNode[] = [];
|
||||
public parentElement: IRRNode | null = null;
|
||||
public parentNode: IRRNode | null = null;
|
||||
public textContent: string | null;
|
||||
public ownerDocument: IRRDocument;
|
||||
public readonly ELEMENT_NODE: number = NodeType.ELEMENT_NODE;
|
||||
public readonly TEXT_NODE: number = NodeType.TEXT_NODE;
|
||||
// corresponding nodeType value of standard HTML Node
|
||||
public readonly nodeType: number;
|
||||
public readonly nodeName: string;
|
||||
public readonly RRNodeType: RRNodeType;
|
||||
|
||||
constructor(...args: any[]) {}
|
||||
|
||||
public get firstChild(): IRRNode | null {
|
||||
return this.childNodes[0] || null;
|
||||
}
|
||||
|
||||
public get lastChild(): IRRNode | null {
|
||||
return this.childNodes[this.childNodes.length - 1] || null;
|
||||
}
|
||||
|
||||
public get nextSibling(): IRRNode | null {
|
||||
let parentNode = this.parentNode;
|
||||
if (!parentNode) return null;
|
||||
const siblings = parentNode.childNodes;
|
||||
let index = siblings.indexOf(this);
|
||||
return siblings[index + 1] || null;
|
||||
}
|
||||
|
||||
public contains(node: IRRNode) {
|
||||
if (node === this) return true;
|
||||
for (const child of this.childNodes) {
|
||||
if (child.contains(node)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public appendChild(_newChild: IRRNode): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public insertBefore(_newChild: IRRNode, _refChild: IRRNode | null): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method.`,
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'RRNode';
|
||||
}
|
||||
}
|
||||
|
||||
export function BaseRRDocumentImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
return class BaseRRDocument extends RRNodeClass implements IRRDocument {
|
||||
public readonly nodeType: number = NodeType.DOCUMENT_NODE;
|
||||
public readonly nodeName: '#document' = '#document';
|
||||
public readonly compatMode: 'BackCompat' | 'CSS1Compat' = 'CSS1Compat';
|
||||
public readonly RRNodeType = RRNodeType.Document;
|
||||
public textContent: string | null = null;
|
||||
|
||||
public get documentElement(): IRRElement | null {
|
||||
return (
|
||||
(this.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'HTML',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get body(): IRRElement | null {
|
||||
return (
|
||||
(this.documentElement?.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'BODY',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get head(): IRRElement | null {
|
||||
return (
|
||||
(this.documentElement?.childNodes.find(
|
||||
(node) =>
|
||||
node.RRNodeType === RRNodeType.Element &&
|
||||
(node as IRRElement).tagName === 'HEAD',
|
||||
) as IRRElement) || null
|
||||
);
|
||||
}
|
||||
|
||||
public get implementation(): IRRDocument {
|
||||
return this;
|
||||
}
|
||||
|
||||
public get firstElementChild(): IRRElement | null {
|
||||
return this.documentElement;
|
||||
}
|
||||
|
||||
public appendChild(childNode: IRRNode): IRRNode {
|
||||
const nodeType = childNode.RRNodeType;
|
||||
if (
|
||||
nodeType === RRNodeType.Element ||
|
||||
nodeType === RRNodeType.DocumentType
|
||||
) {
|
||||
if (this.childNodes.some((s) => s.RRNodeType === nodeType)) {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${
|
||||
nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype'
|
||||
} on RRDocument allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
childNode.parentElement = null;
|
||||
childNode.parentNode = this;
|
||||
this.childNodes.push(childNode);
|
||||
return childNode;
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
const nodeType = newChild.RRNodeType;
|
||||
if (
|
||||
nodeType === RRNodeType.Element ||
|
||||
nodeType === RRNodeType.DocumentType
|
||||
) {
|
||||
if (this.childNodes.some((s) => s.RRNodeType === nodeType)) {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one ${
|
||||
nodeType === RRNodeType.Element ? 'RRElement' : 'RRDoctype'
|
||||
} on RRDocument allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = null;
|
||||
newChild.parentNode = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode) {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.childNodes = [];
|
||||
}
|
||||
|
||||
public close() {}
|
||||
|
||||
/**
|
||||
* Adhoc implementation for setting xhtml namespace in rebuilt.ts (rrweb-snapshot).
|
||||
* There are two lines used this function:
|
||||
* 1. doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">')
|
||||
* 2. doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">')
|
||||
*/
|
||||
public write(content: string) {
|
||||
let publicId;
|
||||
if (
|
||||
content ===
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">'
|
||||
)
|
||||
publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN';
|
||||
else if (
|
||||
content ===
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">'
|
||||
)
|
||||
publicId = '-//W3C//DTD HTML 4.0 Transitional//EN';
|
||||
if (publicId) {
|
||||
const doctype = this.createDocumentType('html', publicId, '');
|
||||
this.open();
|
||||
this.appendChild(doctype);
|
||||
}
|
||||
}
|
||||
|
||||
createDocument(
|
||||
_namespace: string | null,
|
||||
_qualifiedName: string | null,
|
||||
_doctype?: DocumentType | null,
|
||||
): IRRDocument {
|
||||
return new BaseRRDocument();
|
||||
}
|
||||
|
||||
createDocumentType(
|
||||
qualifiedName: string,
|
||||
publicId: string,
|
||||
systemId: string,
|
||||
): IRRDocumentType {
|
||||
const doctype = new (BaseRRDocumentTypeImpl(BaseRRNode))(
|
||||
qualifiedName,
|
||||
publicId,
|
||||
systemId,
|
||||
);
|
||||
doctype.ownerDocument = this;
|
||||
return doctype;
|
||||
}
|
||||
|
||||
createElement(tagName: string): IRRElement {
|
||||
const element = new (BaseRRElementImpl(BaseRRNode))(tagName);
|
||||
element.ownerDocument = this;
|
||||
return element;
|
||||
}
|
||||
|
||||
createElementNS(_namespaceURI: string, qualifiedName: string): IRRElement {
|
||||
return this.createElement(qualifiedName);
|
||||
}
|
||||
|
||||
createTextNode(data: string): IRRText {
|
||||
const text = new (BaseRRTextImpl(BaseRRNode))(data);
|
||||
text.ownerDocument = this;
|
||||
return text;
|
||||
}
|
||||
|
||||
createComment(data: string): IRRComment {
|
||||
const comment = new (BaseRRCommentImpl(BaseRRNode))(data);
|
||||
comment.ownerDocument = this;
|
||||
return comment;
|
||||
}
|
||||
|
||||
createCDATASection(data: string): IRRCDATASection {
|
||||
const CDATASection = new (BaseRRCDATASectionImpl(BaseRRNode))(data);
|
||||
CDATASection.ownerDocument = this;
|
||||
return CDATASection;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'RRDocument';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRDocumentTypeImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRDocumentType
|
||||
extends RRNodeClass
|
||||
implements IRRDocumentType {
|
||||
public readonly nodeType: number = NodeType.DOCUMENT_TYPE_NODE;
|
||||
public readonly RRNodeType = RRNodeType.DocumentType;
|
||||
public readonly nodeName: string;
|
||||
public readonly name: string;
|
||||
public readonly publicId: string;
|
||||
public readonly systemId: string;
|
||||
public textContent: string | null = null;
|
||||
|
||||
constructor(qualifiedName: string, publicId: string, systemId: string) {
|
||||
super();
|
||||
this.name = qualifiedName;
|
||||
this.publicId = publicId;
|
||||
this.systemId = systemId;
|
||||
this.nodeName = qualifiedName;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'RRDocumentType';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRElementImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRElement extends RRNodeClass implements IRRElement {
|
||||
public readonly nodeType: number = NodeType.ELEMENT_NODE;
|
||||
public readonly RRNodeType = RRNodeType.Element;
|
||||
public readonly nodeName: string;
|
||||
public tagName: string;
|
||||
public attributes: Record<string, string> = {};
|
||||
public shadowRoot: IRRElement | null = null;
|
||||
public scrollLeft?: number;
|
||||
public scrollTop?: number;
|
||||
|
||||
constructor(tagName: string) {
|
||||
super();
|
||||
this.tagName = tagName.toUpperCase();
|
||||
this.nodeName = tagName.toUpperCase();
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
let result = '';
|
||||
this.childNodes.forEach((node) => (result += node.textContent));
|
||||
return result;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.childNodes = [this.ownerDocument.createTextNode(textContent)];
|
||||
}
|
||||
|
||||
public get classList(): ClassList {
|
||||
return new ClassList(
|
||||
this.attributes.class as string | undefined,
|
||||
(newClassName) => {
|
||||
this.attributes.class = newClassName;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public get id() {
|
||||
return this.attributes.id || '';
|
||||
}
|
||||
|
||||
public get className() {
|
||||
return this.attributes.class || '';
|
||||
}
|
||||
|
||||
public get style() {
|
||||
const style = (this.attributes.style
|
||||
? parseCSSText(this.attributes.style as string)
|
||||
: {}) as CSSStyleDeclaration;
|
||||
const hyphenateRE = /\B([A-Z])/g;
|
||||
style.setProperty = (
|
||||
name: string,
|
||||
value: string | null,
|
||||
priority?: string,
|
||||
) => {
|
||||
if (hyphenateRE.test(name)) return;
|
||||
const normalizedName = camelize(name);
|
||||
if (!value) delete style[normalizedName];
|
||||
else style[normalizedName] = value;
|
||||
if (priority === 'important') style[normalizedName] += ' !important';
|
||||
this.attributes.style = toCSSText(style);
|
||||
};
|
||||
style.removeProperty = (name: string) => {
|
||||
if (hyphenateRE.test(name)) return '';
|
||||
const normalizedName = camelize(name);
|
||||
const value = style[normalizedName] || '';
|
||||
delete style[normalizedName];
|
||||
this.attributes.style = toCSSText(style);
|
||||
return value;
|
||||
};
|
||||
return style;
|
||||
}
|
||||
|
||||
public getAttribute(name: string) {
|
||||
return this.attributes[name] || null;
|
||||
}
|
||||
|
||||
public setAttribute(name: string, attribute: string) {
|
||||
this.attributes[name] = attribute;
|
||||
}
|
||||
|
||||
public setAttributeNS(
|
||||
_namespace: string | null,
|
||||
qualifiedName: string,
|
||||
value: string,
|
||||
): void {
|
||||
this.setAttribute(qualifiedName, value);
|
||||
}
|
||||
|
||||
public removeAttribute(name: string) {
|
||||
delete this.attributes[name];
|
||||
}
|
||||
|
||||
public appendChild(newChild: IRRNode): IRRNode {
|
||||
this.childNodes.push(newChild);
|
||||
newChild.parentNode = this;
|
||||
newChild.parentElement = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode {
|
||||
if (refChild === null) return this.appendChild(newChild);
|
||||
const childIndex = this.childNodes.indexOf(refChild);
|
||||
if (childIndex == -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(childIndex, 0, newChild);
|
||||
newChild.parentElement = this;
|
||||
newChild.parentNode = this;
|
||||
return newChild;
|
||||
}
|
||||
|
||||
public removeChild(node: IRRNode): IRRNode {
|
||||
const indexOfChild = this.childNodes.indexOf(node);
|
||||
if (indexOfChild === -1)
|
||||
throw new Error(
|
||||
"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode.",
|
||||
);
|
||||
this.childNodes.splice(indexOfChild, 1);
|
||||
node.parentElement = null;
|
||||
node.parentNode = null;
|
||||
return node;
|
||||
}
|
||||
|
||||
public attachShadow(_init: ShadowRootInit): IRRElement {
|
||||
const shadowRoot = this.ownerDocument.createElement('SHADOWROOT');
|
||||
this.shadowRoot = shadowRoot;
|
||||
return shadowRoot;
|
||||
}
|
||||
|
||||
public dispatchEvent(_event: Event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let attributeString = '';
|
||||
for (let attribute in this.attributes) {
|
||||
attributeString += `${attribute}="${this.attributes[attribute]}" `;
|
||||
}
|
||||
return `${this.tagName} ${attributeString}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRMediaElementImpl<
|
||||
RRElement extends ConstrainedConstructor<IRRElement>
|
||||
>(RRElementClass: RRElement) {
|
||||
return class BaseRRMediaElement extends RRElementClass {
|
||||
public currentTime?: number;
|
||||
public volume?: number;
|
||||
public paused?: boolean;
|
||||
public muted?: boolean;
|
||||
attachShadow(_init: ShadowRootInit): IRRElement {
|
||||
throw new Error(
|
||||
`RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`,
|
||||
);
|
||||
}
|
||||
public play() {
|
||||
this.paused = false;
|
||||
}
|
||||
public pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRTextImpl<RRNode extends ConstrainedConstructor<IRRNode>>(
|
||||
RRNodeClass: RRNode,
|
||||
) {
|
||||
// @ts-ignore
|
||||
return class BaseRRText extends RRNodeClass implements IRRText {
|
||||
public readonly nodeType: number = NodeType.TEXT_NODE;
|
||||
public readonly nodeName: '#text' = '#text';
|
||||
public readonly RRNodeType = RRNodeType.Text;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRText text=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRCommentImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRComment extends RRNodeClass implements IRRComment {
|
||||
public readonly nodeType: number = NodeType.COMMENT_NODE;
|
||||
public readonly nodeName: '#comment' = '#comment';
|
||||
public readonly RRNodeType = RRNodeType.Comment;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRComment text=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BaseRRCDATASectionImpl<
|
||||
RRNode extends ConstrainedConstructor<IRRNode>
|
||||
>(RRNodeClass: RRNode) {
|
||||
// @ts-ignore
|
||||
return class BaseRRCDATASection
|
||||
extends RRNodeClass
|
||||
implements IRRCDATASection {
|
||||
public readonly nodeName: '#cdata-section' = '#cdata-section';
|
||||
public readonly nodeType: number = NodeType.CDATA_SECTION_NODE;
|
||||
public readonly RRNodeType = RRNodeType.CDATA;
|
||||
public data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get textContent(): string {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public set textContent(textContent: string) {
|
||||
this.data = textContent;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `RRCDATASection data=${JSON.stringify(this.data)}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class ClassList {
|
||||
private onChange: ((newClassText: string) => void) | undefined;
|
||||
classes: string[] = [];
|
||||
|
||||
constructor(
|
||||
classText?: string,
|
||||
onChange?: ((newClassText: string) => void) | undefined,
|
||||
) {
|
||||
if (classText) {
|
||||
const classes = classText.trim().split(/\s+/);
|
||||
this.classes.push(...classes);
|
||||
}
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
add = (...classNames: string[]) => {
|
||||
for (const item of classNames) {
|
||||
const className = String(item);
|
||||
if (this.classes.indexOf(className) >= 0) continue;
|
||||
this.classes.push(className);
|
||||
}
|
||||
this.onChange && this.onChange(this.classes.join(' '));
|
||||
};
|
||||
|
||||
remove = (...classNames: string[]) => {
|
||||
this.classes = this.classes.filter(
|
||||
(item) => classNames.indexOf(item) === -1,
|
||||
);
|
||||
this.onChange && this.onChange(this.classes.join(' '));
|
||||
};
|
||||
}
|
||||
|
||||
export type CSSStyleDeclaration = Record<string, string> & {
|
||||
setProperty: (
|
||||
name: string,
|
||||
value: string | null,
|
||||
priority?: string | null,
|
||||
) => void;
|
||||
removeProperty: (name: string) => string;
|
||||
};
|
||||
|
||||
// Enumerate nodeType value of standard HTML Node.
|
||||
export enum NodeType {
|
||||
PLACEHOLDER, // This isn't a node type. Enum type value starts from zero but NodeType value starts from 1.
|
||||
ELEMENT_NODE,
|
||||
ATTRIBUTE_NODE,
|
||||
TEXT_NODE,
|
||||
CDATA_SECTION_NODE,
|
||||
ENTITY_REFERENCE_NODE,
|
||||
ENTITY_NODE,
|
||||
PROCESSING_INSTRUCTION_NODE,
|
||||
COMMENT_NODE,
|
||||
DOCUMENT_NODE,
|
||||
DOCUMENT_TYPE_NODE,
|
||||
DOCUMENT_FRAGMENT_NODE,
|
||||
}
|
||||
Reference in New Issue
Block a user