blob: 69ce25b9f15e7018abc907e71aadc46a546b526f [file] [log] [blame]
/**
* @license Copyright 2017 Google Inc. All Rights Reserved.
* 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.
*/
'use strict';
/* globals URL self */
class DOM {
/**
* @param {Document} document
*/
constructor(document) {
/** @type {Document} */
this._document = document;
}
// TODO(bckenny): can pass along `createElement`'s inferred type
/**
* @param {string} name
* @param {string=} className
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {Element}
*/
createElement(name, className, attrs = {}) {
const element = this._document.createElement(name);
if (className) {
element.className = className;
}
Object.keys(attrs).forEach(key => {
const value = attrs[key];
if (typeof value !== 'undefined') {
element.setAttribute(key, value);
}
});
return element;
}
/**
* @return {DocumentFragment}
*/
createFragment() {
return this._document.createDocumentFragment();
}
/**
* @param {Element} parentElem
* @param {string} elementName
* @param {string=} className
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {Element}
*/
createChildOf(parentElem, elementName, className, attrs) {
const element = this.createElement(elementName, className, attrs);
parentElem.appendChild(element);
return element;
}
/**
* @param {string} selector
* @param {ParentNode} context
* @return {DocumentFragment} A clone of the template content.
* @throws {Error}
*/
cloneTemplate(selector, context) {
const template = /** @type {?HTMLTemplateElement} */ (context.querySelector(selector));
if (!template) {
throw new Error(`Template not found: template${selector}`);
}
const clone = this._document.importNode(template.content, true);
// Prevent duplicate styles in the DOM. After a template has been stamped
// for the first time, remove the clone's styles so they're not re-added.
if (template.hasAttribute('data-stamped')) {
this.findAll('style', clone).forEach(style => style.remove());
}
template.setAttribute('data-stamped', 'true');
return clone;
}
/**
* Resets the "stamped" state of the templates.
*/
resetTemplates() {
this.findAll('template[data-stamped]', this._document).forEach(t => {
t.removeAttribute('data-stamped');
});
}
/**
* @param {string} text
* @return {Element}
*/
convertMarkdownLinkSnippets(text) {
const element = this.createElement('span');
// Split on markdown links (e.g. [some link](https://...)).
const parts = text.split(/\[([^\]]*?)\]\((https?:\/\/.*?)\)/g);
while (parts.length) {
// Pop off the same number of elements as there are capture groups.
const [preambleText, linkText, linkHref] = parts.splice(0, 3);
element.appendChild(this._document.createTextNode(preambleText));
// Append link if there are any.
if (linkText && linkHref) {
const a = /** @type {HTMLAnchorElement} */ (this.createElement('a'));
a.rel = 'noopener';
a.target = '_blank';
a.textContent = linkText;
a.href = (new URL(linkHref)).href;
element.appendChild(a);
}
}
return element;
}
/**
* @param {string} text
* @return {Element}
*/
convertMarkdownCodeSnippets(text) {
const element = this.createElement('span');
const parts = text.split(/`(.*?)`/g); // Split on markdown code slashes
while (parts.length) {
// Pop off the same number of elements as there are capture groups.
const [preambleText, codeText] = parts.splice(0, 2);
element.appendChild(this._document.createTextNode(preambleText));
if (codeText) {
const pre = /** @type {HTMLPreElement} */ (this.createElement('code'));
pre.textContent = codeText;
element.appendChild(pre);
}
}
return element;
}
/**
* @return {Document}
*/
document() {
return this._document;
}
/**
* TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
* @return {boolean}
*/
isDevTools() {
return !!this._document.querySelector('.lh-devtools');
}
/**
* Guaranteed context.querySelector. Always returns an element or throws if
* nothing matches query.
* @param {string} query
* @param {ParentNode} context
* @return {HTMLElement}
*/
find(query, context) {
/** @type {?HTMLElement} */
const result = context.querySelector(query);
if (result === null) {
throw new Error(`query ${query} not found`);
}
return result;
}
/**
* Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
* @param {string} query
* @param {ParentNode} context
* @return {Array<HTMLElement>}
*/
findAll(query, context) {
return Array.from(context.querySelectorAll(query));
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = DOM;
} else {
self.DOM = DOM;
}