diff --git a/clients/extension/scripts/popup.js b/clients/extension/scripts/popup.js index 016c435bc06b..2cd5be865bd7 100644 --- a/clients/extension/scripts/popup.js +++ b/clients/extension/scripts/popup.js @@ -5,6 +5,9 @@ */ import * as SettingsController from './settings-controller.js'; +import {DOM} from '../../../report/renderer/dom.js'; + +const dom = new DOM(document, document.documentElement); // Replaced with 'chrome' or 'firefox' in the build script. /** @type {string} */ @@ -22,21 +25,6 @@ const FIREFOX_STRINGS = { const STRINGS = BROWSER_BRAND === 'chrome' ? CHROME_STRINGS : FIREFOX_STRINGS; -/** - * Guaranteed context.querySelector. Always returns an element or throws if - * nothing matches query. - * @template {string} T - * @param {T} query - * @param {ParentNode=} context - */ -function find(query, context = document) { - const result = context.querySelector(query); - if (result === null) { - throw new Error(`query ${query} not found`); - } - return result; -} - /** * @param {string} text * @param {string} id @@ -129,7 +117,7 @@ function generateCategoryOptionsList(settings) { frag.append(createOptionItem(category.title, category.id, isChecked)); }); - const optionsCategoriesList = find('.options__categories'); + const optionsCategoriesList = dom.find('.options__categories'); optionsCategoriesList.append(frag); } @@ -146,7 +134,7 @@ function generateBackendOptionsList(settings) { frag.append(createRadioItem('backend', backend.title, backend.id, isChecked)); }); - const optionsCategoriesList = find('.options__backend'); + const optionsCategoriesList = dom.find('.options__backend'); optionsCategoriesList.append(frag); } @@ -214,7 +202,7 @@ function generateLocaleOptionsList(settings) { frag.append(optionEl); }); - const optionsLocalesList = find('.options__locales'); + const optionsLocalesList = dom.find('.options__locales'); optionsLocalesList.append(frag); } @@ -222,12 +210,12 @@ function generateLocaleOptionsList(settings) { * @param {SettingsController.Settings} settings */ function configureVisibleSettings(settings) { - const optionsCategoriesList = find('.options__categories'); + const optionsCategoriesList = dom.find('.options__categories'); optionsCategoriesList.parentElement?.classList.toggle('hidden', settings.backend === 'psi'); } function fillDevToolsShortcut() { - const el = find('.devtools-shortcut'); + const el = dom.find('.devtools-shortcut'); const isMac = /mac/i.test(navigator.platform); el.textContent = isMac ? '⌘⌥I (Cmd+Opt+I)' : 'F12'; } @@ -237,13 +225,13 @@ function fillDevToolsShortcut() { * @return {SettingsController.Settings} */ function readSettingsFromDomAndPersist() { - const optionsEl = find('.section--options'); + const optionsEl = dom.find('.section--options'); // Save settings when options page is closed. - const backend = find('.options__backend input:checked').value; - const locale = find('select.options__locales').value; + const backend = dom.find('.options__backend input:checked').value; + const locale = dom.find('select.options__locales').value; const checkboxes = optionsEl.querySelectorAll('.options__categories input:checked'); const selectedCategories = Array.from(checkboxes).map(input => input.value); - const device = find('input[name="device"]:checked').value; + const device = dom.find('input[name="device"]:checked').value; const settings = { backend, @@ -284,13 +272,13 @@ async function initPopup() { if (BROWSER_BRAND === 'chrome') { fillDevToolsShortcut(); } - const browserBrandEl = find(`.browser-brand--${BROWSER_BRAND}`); + const browserBrandEl = dom.find(`.browser-brand--${BROWSER_BRAND}`); browserBrandEl.classList.remove('hidden'); - const generateReportButton = find('button.button--generate'); - const psiDisclaimerEl = find('.psi-disclaimer'); - const errorMessageEl = find('.errormsg'); - const optionsFormEl = find('.options__form'); + const generateReportButton = dom.find('button.button--generate'); + const psiDisclaimerEl = dom.find('.psi-disclaimer'); + const errorMessageEl = dom.find('.errormsg'); + const optionsFormEl = dom.find('.options__form'); /** @type {URL} */ let siteUrl; @@ -314,7 +302,7 @@ async function initPopup() { generateCategoryOptionsList(settings); generateLocaleOptionsList(settings); configureVisibleSettings(settings); - const selectedDeviceEl = find(`.options__device input[value="${settings.device}"]`); + const selectedDeviceEl = dom.find(`.options__device input[value="${settings.device}"]`); selectedDeviceEl.checked = true; generateReportButton.addEventListener('click', () => { diff --git a/report/renderer/dom.js b/report/renderer/dom.js index 00db13f10053..246724332fbb 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -259,7 +259,7 @@ export class DOM { * @param {ParentNode} context * @return {ParseSelector} */ - find(query, context) { + find(query, context = this.rootEl ?? this._document) { const result = this.maybeFind(query, context); if (result === null) { throw new Error(`query ${query} not found`); diff --git a/treemap/app/src/main.js b/treemap/app/src/main.js index 299bf3e03832..30a9feda4ca7 100644 --- a/treemap/app/src/main.js +++ b/treemap/app/src/main.js @@ -14,9 +14,12 @@ import {GithubApi} from '../../../viewer/app/src/github-api.js'; import {I18nFormatter} from '../../../report/renderer/i18n-formatter.js'; import {TextEncoding} from '../../../report/renderer/text-encoding.js'; import {Logger} from '../../../report/renderer/logger.js'; +import {DOM} from '../../../report/renderer/dom.js'; /** @typedef {LH.Treemap.Node & {dom?: HTMLElement}} NodeWithElement */ +const dom = new DOM(document, document.documentElement); + const DUPLICATED_MODULES_IGNORE_THRESHOLD = 1024; const DUPLICATED_MODULES_IGNORE_ROOT_RATIO = 0.01; @@ -129,7 +132,7 @@ class TreemapViewer { } createHeader() { - const urlEl = TreemapUtil.find('a.lh-header--url'); + const urlEl = dom.find('a.lh-header--url'); urlEl.textContent = this.documentUrl.toString(); urlEl.href = this.documentUrl.toString(); @@ -137,7 +140,7 @@ class TreemapViewer { } createBundleSelector() { - const bundleSelectorEl = TreemapUtil.find('select.bundle-selector'); + const bundleSelectorEl = dom.find('select.bundle-selector'); bundleSelectorEl.textContent = ''; // Clear just in case document was saved with Ctrl+S. /** @type {LH.Treemap.Selector[]} */ @@ -148,7 +151,7 @@ class TreemapViewer { * @param {string} text */ function makeOption(selector, text) { - const optionEl = TreemapUtil.createChildOf(bundleSelectorEl, 'option'); + const optionEl = dom.createChildOf(bundleSelectorEl, 'option'); optionEl.value = String(selectors.length); selectors.push(selector); optionEl.textContent = text; @@ -183,7 +186,7 @@ class TreemapViewer { initListeners() { const options = {signal: this.abortController.signal}; - const treemapEl = TreemapUtil.find('.lh-treemap'); + const treemapEl = dom.find('.lh-treemap'); const resizeObserver = new ResizeObserver(() => this.resize()); resizeObserver.observe(treemapEl); @@ -222,7 +225,7 @@ class TreemapViewer { nodeEl.classList.remove('webtreemap-node--hover'); }, options); - TreemapUtil.find('.lh-table').addEventListener('mouseover', e => { + dom.find('.lh-table').addEventListener('mouseover', e => { const target = e.target; if (!(target instanceof HTMLElement)) return; @@ -240,7 +243,7 @@ class TreemapViewer { }, {once: true}); }, options); - const toggleTableBtn = TreemapUtil.find('.lh-button--toggle-table'); + const toggleTableBtn = dom.find('.lh-button--toggle-table'); toggleTableBtn.addEventListener('click', () => treemapViewer.toggleTable(), options); } @@ -430,7 +433,7 @@ class TreemapViewer { }); this.el.textContent = ''; this.treemap.render(this.el); - TreemapUtil.find('.webtreemap-node').classList.add('webtreemap-node--root'); + dom.find('.webtreemap-node').classList.add('webtreemap-node--root'); this.createTable(); } @@ -447,7 +450,7 @@ class TreemapViewer { } createTable() { - const tableEl = TreemapUtil.find('.lh-table'); + const tableEl = dom.find('.lh-table'); tableEl.textContent = ''; /** @type {Array<{node: NodeWithElement, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */ @@ -550,15 +553,15 @@ class TreemapViewer { /** @type {typeof data[number]} */ const dataRow = cell.getRow().getData(); - const el = TreemapUtil.createElement('div', 'lh-coverage-bar'); + const el = dom.createElement('div', 'lh-coverage-bar'); if (dataRow.unusedBytes === undefined) return el; el.style.setProperty('--max', String(maxSize)); el.style.setProperty('--used', String(dataRow.resourceBytes - dataRow.unusedBytes)); el.style.setProperty('--unused', String(dataRow.unusedBytes)); - TreemapUtil.createChildOf(el, 'div', 'lh-coverage-bar--used'); - TreemapUtil.createChildOf(el, 'div', 'lh-coverage-bar--unused'); + dom.createChildOf(el, 'div', 'lh-coverage-bar--used'); + dom.createChildOf(el, 'div', 'lh-coverage-bar--unused'); return el; }}, @@ -573,9 +576,9 @@ class TreemapViewer { * @param {boolean=} show */ toggleTable(show) { - const mainEl = TreemapUtil.find('main'); + const mainEl = dom.find('main'); mainEl.classList.toggle('lh-main--show-table', show); - const buttonEl = TreemapUtil.find('.lh-button--toggle-table'); + const buttonEl = dom.find('.lh-button--toggle-table'); buttonEl.classList.toggle('lh-button--active', show); } @@ -669,20 +672,20 @@ function renderViewModeButtons(viewModes) { * @param {LH.Treemap.ViewMode} viewMode */ function render(viewMode) { - const viewModeEl = TreemapUtil.createChildOf(viewModesEl, 'div', 'view-mode'); + const viewModeEl = dom.createChildOf(viewModesEl, 'div', 'view-mode'); if (!viewMode.enabled) viewModeEl.classList.add('view-mode--disabled'); viewModeEl.id = `view-mode--${viewMode.id}`; - const inputEl = TreemapUtil.createChildOf(viewModeEl, 'input', 'view-mode__button'); + const inputEl = dom.createChildOf(viewModeEl, 'input', 'view-mode__button'); inputEl.id = `view-mode--${viewMode.id}__label`; inputEl.type = 'radio'; inputEl.name = 'view-mode'; inputEl.disabled = !viewMode.enabled; - const labelEl = TreemapUtil.createChildOf(viewModeEl, 'label'); + const labelEl = dom.createChildOf(viewModeEl, 'label'); labelEl.htmlFor = inputEl.id; - TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__label').textContent = viewMode.label; - TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__sublabel lh-text-dim').textContent = + dom.createChildOf(labelEl, 'span', 'view-mode__label').textContent = viewMode.label; + dom.createChildOf(labelEl, 'span', 'view-mode__sublabel lh-text-dim').textContent = ` (${viewMode.subLabel})`; inputEl.addEventListener('click', () => { @@ -691,7 +694,7 @@ function renderViewModeButtons(viewModes) { }); } - const viewModesEl = TreemapUtil.find('.lh-modes'); + const viewModesEl = dom.find('.lh-modes'); viewModesEl.textContent = ''; viewModes.forEach(render); } @@ -701,7 +704,7 @@ function renderViewModeButtons(viewModes) { * @param {HTMLElement} el */ function applyActiveClass(currentViewModeId, el) { - const viewModesEl = TreemapUtil.find('.lh-modes'); + const viewModesEl = dom.find('.lh-modes'); for (const viewModeEl of viewModesEl.querySelectorAll('.view-mode')) { if (!(viewModeEl instanceof HTMLElement)) continue; @@ -721,7 +724,7 @@ function injectOptions(options) { scriptEl.remove(); } - scriptEl = TreemapUtil.createChildOf(document.head, 'script', 'lh-injectedoptions'); + scriptEl = dom.createChildOf(document.head, 'script', 'lh-injectedoptions'); scriptEl.textContent = ` window.__treemapOptions = ${JSON.stringify(options)}; `; @@ -742,7 +745,7 @@ class LighthouseTreemap { document.addEventListener('paste', this._onPaste); // Hidden file input to trigger manual file selector. - const fileInput = TreemapUtil.find('input#hidden-file-input', document); + const fileInput = dom.find('input#hidden-file-input', document); fileInput.addEventListener('change', e => { if (!e.target) { return; @@ -758,7 +761,7 @@ class LighthouseTreemap { }); // A click on the visual placeholder will trigger the hidden file input. - const placeholderTarget = TreemapUtil.find('.treemap-placeholder-inner', document); + const placeholderTarget = dom.find('.treemap-placeholder-inner', document); placeholderTarget.addEventListener('click', e => { const target = /** @type {?Element} */ (e.target); @@ -772,8 +775,8 @@ class LighthouseTreemap { * @param {LH.Treemap.Options} options */ init(options) { - TreemapUtil.find('.treemap-placeholder').classList.add('hidden'); - TreemapUtil.find('main').classList.remove('hidden'); + dom.find('.treemap-placeholder').classList.add('hidden'); + dom.find('main').classList.remove('hidden'); const locale = options.lhr.configSettings.locale; document.documentElement.lang = locale; @@ -792,11 +795,11 @@ class LighthouseTreemap { } if (treemapViewer) { - TreemapUtil.find('.lh-treemap').textContent = ''; - TreemapUtil.find('.lh-table').textContent = ''; + dom.find('.lh-treemap').textContent = ''; + dom.find('.lh-table').textContent = ''; treemapViewer.abortController.abort(); } - treemapViewer = new TreemapViewer(options, TreemapUtil.find('div.lh-treemap')); + treemapViewer = new TreemapViewer(options, dom.find('div.lh-treemap')); injectOptions(options); diff --git a/treemap/app/src/util.js b/treemap/app/src/util.js index 881d8b67b88f..0287dae3d42b 100644 --- a/treemap/app/src/util.js +++ b/treemap/app/src/util.js @@ -6,8 +6,6 @@ /* eslint-env browser */ -/** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */ -/** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector} ParseSelector */ /** @typedef {import('../../../report/renderer/i18n-formatter').I18nFormatter} I18nFormatter */ const UIStrings = { @@ -106,52 +104,6 @@ class TreemapUtil { return url.toString().replace(fromRelativeUrl.origin, ''); } - /** - * @template {string} T - * @param {T} name - * @param {string=} className - * @return {HTMLElementByTagName[T]} - */ - static createElement(name, className) { - const element = document.createElement(name); - if (className) { - element.className = className; - } - return element; - } - - /** - * @template {string} T - * @param {Element} parentElem - * @param {T} elementName - * @param {string=} className - * @return {HTMLElementByTagName[T]} - */ - static createChildOf(parentElem, elementName, className) { - const element = this.createElement(elementName, className); - parentElem.append(element); - return element; - } - - /** - * Guaranteed context.querySelector. Always returns an element or throws if - * nothing matches query. - * @template {string} T - * @param {T} query - * @param {ParentNode=} context - * @return {ParseSelector} - */ - static find(query, context = document) { - const result = context.querySelector(query); - if (result === null) { - throw new Error(`query ${query} not found`); - } - // Because we control the treemap layout and templates, use the simpler - // `typed-query-selector` types that don't require differentiating between - // e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011 - return /** @type {ParseSelector} */ (result); - } - /** * Given a list of items, return a function (a hasher) that will map keys to an item. * When a key is seen for the first time, the item returned is cached and will always diff --git a/viewer/app/src/lighthouse-report-viewer.js b/viewer/app/src/lighthouse-report-viewer.js index 6730bf674559..1e764b7cb640 100644 --- a/viewer/app/src/lighthouse-report-viewer.js +++ b/viewer/app/src/lighthouse-report-viewer.js @@ -15,25 +15,13 @@ import {ReportRenderer} from '../../../report/renderer/report-renderer.js'; import {TextEncoding} from '../../../report/renderer/text-encoding.js'; import {renderFlowReport} from '../../../flow-report/api'; +// @ts-expect-error Legacy use of report renderer +const dom = new DOM(document); + /* global logger ReportGenerator */ /** @typedef {import('./psi-api').PSIParams} PSIParams */ -/** - * Guaranteed context.querySelector. Always returns an element or throws if - * nothing matches query. - * @template {string} T - * @param {T} query - * @param {ParentNode} context - */ -function find(query, context) { - const result = context.querySelector(query); - if (result === null) { - throw new Error(`query ${query} not found`); - } - return result; -} - /** * Class that manages viewing Lighthouse reports. */ @@ -72,11 +60,11 @@ export class LighthouseReportViewer { _addEventListeners() { document.addEventListener('paste', this._onPaste); - const gistUrlInput = find('.js-gist-url', document); + const gistUrlInput = dom.find('.js-gist-url'); gistUrlInput.addEventListener('change', this._onUrlInputChange); // Hidden file input to trigger manual file selector. - const fileInput = find('input#hidden-file-input', document); + const fileInput = dom.find('input#hidden-file-input'); fileInput.addEventListener('change', e => { if (!e.target) { return; @@ -91,7 +79,7 @@ export class LighthouseReportViewer { inputTarget.value = ''; }); - const selectFileEl = find('.viewer-placeholder__file-button', document); + const selectFileEl = dom.find('.viewer-placeholder__file-button'); selectFileEl.addEventListener('click', _ => { fileInput.click(); }); @@ -228,8 +216,6 @@ export class LighthouseReportViewer { return; } - // @ts-expect-error Legacy use of report renderer - const dom = new DOM(document); const renderer = new ReportRenderer(dom); renderer.renderReport(json, rootEl, { @@ -272,7 +258,7 @@ export class LighthouseReportViewer { // TODO: Really, `json` should really have type `unknown` and // we can have _validateReportJson verify that it's an LH.Result _replaceReportHtml(json) { - const container = find('main', document); + const container = dom.find('main', document); // Reset container content. container.textContent = '';