Cleanup domTree.js to re-use code (#1305)

* Refactor span and anchor into common HtmlDomContainer superclass,
  without changing internal data structure (e.g. not storing tagName).
* Makes it easy to add more similar tags
* Number of lines falls from 676 to 587
This commit is contained in:
Erik Demaine
2018-05-11 06:31:21 -04:00
committed by ylemkimon
parent 523df299e5
commit 9bb48b83f1

View File

@@ -53,24 +53,16 @@ export type SvgChildNode = pathNode | lineNode;
export type CssStyle = {[name: string]: string};
/**
* This node represents a span node, with a className, a list of children, and
* an inline style. It also contains information about its height, depth, and
* maxFontSize.
*
* Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan
* otherwise. This typesafety is important when HTML builders access a span's
* children.
*/
class span<ChildType: VirtualNodeInterface> implements HtmlDomNode {
classes: string[];
export class HtmlDomContainer<ChildType: VirtualNodeInterface>
implements HtmlDomNode {
children: ChildType[];
attributes: {[string]: string};
classes: string[];
height: number;
depth: number;
width: ?number;
maxFontSize: number;
style: CssStyle;
attributes: {[string]: string};
constructor(
classes?: string[],
@@ -80,11 +72,11 @@ class span<ChildType: VirtualNodeInterface> implements HtmlDomNode {
) {
this.classes = classes || [];
this.children = children || [];
this.attributes = {};
this.height = 0;
this.depth = 0;
this.maxFontSize = 0;
this.style = Object.assign({}, style);
this.attributes = {};
if (options) {
if (options.style.isTight()) {
this.classes.push("mtight");
@@ -97,9 +89,9 @@ class span<ChildType: VirtualNodeInterface> implements HtmlDomNode {
}
/**
* Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
* browsers support attributes the same, and having too many custom attributes
* is probably bad.
* Sets an arbitrary attribute on the node. Warning: use this wisely. Not
* all browsers support attributes the same, and having too many custom
* attributes is probably bad.
*/
setAttribute(attribute: string, value: string) {
this.attributes[attribute] = value;
@@ -109,190 +101,57 @@ class span<ChildType: VirtualNodeInterface> implements HtmlDomNode {
return utils.contains(this.classes, className);
}
/**
* Try to combine with given sibling. Returns true if the sibling has
* been successfully merged into this node, and false otherwise.
* Default behavior fails (returns false).
*/
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
tagName(): string {
throw new Error("use of generic HtmlDomContainer tagName");
}
/**
* Convert the span into an HTML node
* Convert into an HTML node
*/
toNode(): HTMLSpanElement {
const span = document.createElement("span");
toNode(): HTMLElement {
const node = document.createElement(this.tagName());
// Apply the class
span.className = createClass(this.classes);
node.className = createClass(this.classes);
// Apply inline styles
for (const style in this.style) {
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
// $FlowFixMe Flow doesn't seem to understand span.style's type.
span.style[style] = this.style[style];
// $FlowFixMe Flow doesn't seem to understand node.style's type.
node.style[style] = this.style[style];
}
}
// Apply attributes
for (const attr in this.attributes) {
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
span.setAttribute(attr, this.attributes[attr]);
if (this.attributes.hasOwnProperty(attr)) {
node.setAttribute(attr, this.attributes[attr]);
}
}
// Append the children, also as HTML nodes
for (let i = 0; i < this.children.length; i++) {
span.appendChild(this.children[i].toNode());
node.appendChild(this.children[i].toNode());
}
return span;
return node;
}
/**
* Convert the span into an HTML markup string
* Convert into an HTML markup string
*/
toMarkup(): string {
let markup = "<span";
let markup = "<" + this.tagName();
// Add the class
if (this.classes.length) {
markup += " class=\"";
markup += utils.escape(createClass(this.classes));
markup += "\"";
}
let styles = "";
// Add the styles, after hyphenation
for (const style in this.style) {
if (this.style.hasOwnProperty(style)) {
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
}
}
if (styles) {
markup += " style=\"" + utils.escape(styles) + "\"";
}
// Add the attributes
for (const attr in this.attributes) {
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
markup += " " + attr + "=\"";
markup += utils.escape(this.attributes[attr]);
markup += "\"";
}
}
markup += ">";
// Add the markup of the children, also as markup
for (let i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
markup += "</span>";
return markup;
}
}
/**
* This node represents an anchor (<a>) element with a hyperlink, a list of classes,
* a list of children, and an inline style. It also contains information about its
* height, depth, and maxFontSize.
*/
class anchor implements HtmlDomNode {
href: string;
classes: string[];
children: HtmlDomNode[];
height: number;
depth: number;
maxFontSize: number;
style: CssStyle;
attributes: {[string]: string};
constructor(
href: string,
classes: string[],
children: HtmlDomNode[],
options: Options,
) {
this.href = href;
this.classes = classes;
this.children = children;
this.height = 0;
this.depth = 0;
this.maxFontSize = 0;
this.style = {};
this.attributes = {};
if (options.style.isTight()) {
this.classes.push("mtight");
}
const color = options.getColor();
if (color) {
this.style.color = color;
}
}
/**
* Sets an arbitrary attribute on the anchor. Warning: use this wisely. Not all
* browsers support attributes the same, and having too many custom attributes
* is probably bad.
*/
setAttribute(attribute: string, value: string) {
this.attributes[attribute] = value;
}
hasClass(className: string): boolean {
return utils.contains(this.classes, className);
}
tryCombine(sibling: HtmlDomNode): boolean {
return false;
}
/**
* Convert the anchor into an HTML node
*/
toNode(): HTMLAnchorElement {
const a = document.createElement("a");
// Apply the href
a.setAttribute('href', this.href);
// Apply the class
if (this.classes.length) {
a.className = createClass(this.classes);
}
// Apply inline styles
for (const style in this.style) {
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
// $FlowFixMe Flow doesn't seem to understand a.style's type.
a.style[style] = this.style[style];
}
}
// Apply attributes
for (const attr in this.attributes) {
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
a.setAttribute(attr, this.attributes[attr]);
}
}
// Append the children, also as HTML nodes
for (let i = 0; i < this.children.length; i++) {
a.appendChild(this.children[i].toNode());
}
return a;
}
/**
* Convert the a into an HTML markup string
*/
toMarkup(): string {
let markup = "<a";
// Add the href
markup += ` href="${utils.escape(this.href)}"`;
// Add the class
if (this.classes.length) {
markup += ` class="${utils.escape(createClass(this.classes))}"`;
@@ -308,30 +167,78 @@ class anchor implements HtmlDomNode {
}
if (styles) {
markup += " style=\"" + utils.escape(styles) + "\"";
markup += ` style="${utils.escape(styles)}"`;
}
// Add the attributes
for (const attr in this.attributes) {
if (attr !== "href" &&
Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`;
if (this.attributes.hasOwnProperty(attr)) {
markup += " " + attr + "=\"";
markup += utils.escape(this.attributes[attr]);
markup += "\"";
}
}
markup += ">";
// Add the markup of the children, also as markup
for (const child of this.children) {
markup += child.toMarkup();
for (let i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
markup += "</a>";
markup += `</${this.tagName()}>`;
return markup;
}
}
/**
* This node represents a span node, with a className, a list of children, and
* an inline style. It also contains information about its height, depth, and
* maxFontSize.
*
* Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan
* otherwise. This typesafety is important when HTML builders access a span's
* children.
*/
class span<ChildType: VirtualNodeInterface> extends HtmlDomContainer<ChildType> {
constructor(
classes?: string[],
children?: ChildType[],
options?: Options,
style?: CssStyle,
) {
super(classes, children, options, style);
}
tagName() {
return "span";
}
}
/**
* This node represents an anchor (<a>) element with a hyperlink, a list of classes,
* a list of children, and an inline style. It also contains information about its
* height, depth, and maxFontSize.
*/
class anchor extends HtmlDomContainer<HtmlDomNode> {
href: string;
constructor(
href: string,
classes: string[],
children: HtmlDomNode[],
options: Options,
) {
super(classes, children, options);
this.setAttribute('href', href);
}
tagName() {
return "a";
}
}
/**
* This node represents a document fragment, which contains elements, but when
* placed into the DOM doesn't have any representation itself. Thus, it only