/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/elements/moz-button-group.mjs"; import { MigrationWizardConstants } from "chrome://browser/content/migration/migration-wizard-constants.mjs"; /** * This component contains the UI that steps users through migrating their * data from other browsers to this one. This component only contains very * basic logic and structure for the UI, and most of the state management * occurs in the MigrationWizardChild JSWindowActor. */ export class MigrationWizard extends HTMLElement { static #template = null; #deck = null; #browserProfileSelector = null; #browserProfileSelectorList = null; #resourceTypeList = null; #shadowRoot = null; #importButton = null; #importFromFileButton = null; #chooseImportFromFile = null; #safariPermissionButton = null; #safariPasswordImportSkipButton = null; #safariPasswordImportSelectButton = null; #selectAllCheckbox = null; #resourceSummary = null; #expandedDetails = false; static get markup() { return ` `; } static get fragment() { if (!MigrationWizard.#template) { let parser = new DOMParser(); let doc = parser.parseFromString(MigrationWizard.markup, "text/html"); MigrationWizard.#template = document.importNode( doc.querySelector("template"), true ); } let fragment = MigrationWizard.#template.content.cloneNode(true); if (window.IS_STORYBOOK) { // If we're using Storybook, load the CSS from the static local file // system rather than chrome:// to take advantage of auto-reloading. fragment.querySelector("link[rel=stylesheet]").href = "./migration/migration-wizard.css"; } return fragment; } constructor() { super(); const shadow = this.attachShadow({ mode: "closed" }); if (window.MozXULElement) { window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl"); window.MozXULElement.insertFTLIfNeeded("browser/migrationWizard.ftl"); } document.l10n.connectRoot(shadow); shadow.appendChild(MigrationWizard.fragment); this.#deck = shadow.querySelector("#wizard-deck"); this.#browserProfileSelector = shadow.querySelector( "#browser-profile-selector" ); this.#resourceSummary = shadow.querySelector("#resource-selection-summary"); this.#resourceSummary.addEventListener("click", this); let cancelCloseButtons = shadow.querySelectorAll(".cancel-close"); for (let button of cancelCloseButtons) { button.addEventListener("click", this); } let finishButtons = shadow.querySelectorAll(".finish-button"); for (let button of finishButtons) { button.addEventListener("click", this); } this.#importButton = shadow.querySelector("#import"); this.#importButton.addEventListener("click", this); this.#importFromFileButton = shadow.querySelector("#import-from-file"); this.#importFromFileButton.addEventListener("click", this); this.#chooseImportFromFile = shadow.querySelector( "#choose-import-from-file" ); this.#chooseImportFromFile.addEventListener("click", this); this.#browserProfileSelector.addEventListener("click", this); this.#resourceTypeList = shadow.querySelector("#resource-type-list"); this.#resourceTypeList.addEventListener("change", this); this.#safariPermissionButton = shadow.querySelector( "#safari-request-permissions" ); this.#safariPermissionButton.addEventListener("click", this); this.#selectAllCheckbox = shadow.querySelector("#select-all").control; this.#safariPasswordImportSkipButton = shadow.querySelector( "#safari-password-import-skip" ); this.#safariPasswordImportSkipButton.addEventListener("click", this); this.#safariPasswordImportSelectButton = shadow.querySelector( "#safari-password-import-select" ); this.#safariPasswordImportSelectButton.addEventListener("click", this); this.#shadowRoot = shadow; } connectedCallback() { if (this.hasAttribute("auto-request-state")) { this.requestState(); } } requestState() { this.dispatchEvent( new CustomEvent("MigrationWizard:RequestState", { bubbles: true }) ); } /** * This setter can be used in the event that the MigrationWizard is being * inserted via Lit, and the caller wants to set state declaratively using * a property expression. * * @param {object} state * The state object to pass to setState. * @see MigrationWizard.setState. */ set state(state) { this.setState(state); } /** * This is the main entrypoint for updating the state and appearance of * the wizard. * * @param {object} state The state to be represented by the component. * @param {string} state.page The page of the wizard to display. This should * be one of the MigrationWizardConstants.PAGES constants. */ setState(state) { switch (state.page) { case MigrationWizardConstants.PAGES.SELECTION: { this.#onShowingSelection(state); break; } case MigrationWizardConstants.PAGES.PROGRESS: { this.#onShowingProgress(state); break; } case MigrationWizardConstants.PAGES.FILE_IMPORT_PROGRESS: { this.#onShowingFileImportProgress(state); break; } case MigrationWizardConstants.PAGES.NO_BROWSERS_FOUND: { this.#onShowingNoBrowsersFound(state); break; } } this.#deck.toggleAttribute( "aria-busy", state.page == MigrationWizardConstants.PAGES.LOADING ); this.#deck.setAttribute("selected-view", `page-${state.page}`); if (window.IS_STORYBOOK) { this.#updateForStorybook(); } } get #dialogMode() { return this.hasAttribute("dialog-mode"); } #ensureSelectionDropdown() { if (this.#browserProfileSelectorList) { return; } this.#browserProfileSelectorList = this.querySelector("panel-list"); if (!this.#browserProfileSelectorList) { throw new Error( "Could not find a under the MigrationWizard during initialization." ); } this.#browserProfileSelectorList.toggleAttribute( "min-width-from-anchor", true ); this.#browserProfileSelectorList.addEventListener("click", this); // Until bug 1823489 is fixed, this is the easiest way for the // migration wizard to style the selector dropdown so that it more // closely lines up with the edges of the selector button. this.#browserProfileSelectorList.style.boxSizing = "border-box"; this.#browserProfileSelectorList.style.overflowY = "auto"; } /** * Reacts to changes to the browser / profile selector dropdown. This * should update the list of resource types to match what's supported * by the selected migrator and profile. * * @param {Element} panelItem the selected */ #onBrowserProfileSelectionChanged(panelItem) { this.#browserProfileSelector.selectedPanelItem = panelItem; this.#browserProfileSelector.querySelector("#migrator-name").textContent = panelItem.displayName; this.#browserProfileSelector.querySelector("#profile-name").textContent = panelItem.profile?.name || ""; if (panelItem.brandImage) { this.#browserProfileSelector.querySelector( ".migrator-icon" ).style.content = `url(${panelItem.brandImage})`; } else { this.#browserProfileSelector.querySelector( ".migrator-icon" ).style.content = "url(chrome://global/skin/icons/defaultFavicon.svg)"; } let key = panelItem.getAttribute("key"); let resourceTypes = panelItem.resourceTypes; for (let child of this.#resourceTypeList.querySelectorAll( "label[data-resource-type]" )) { child.hidden = true; child.control.checked = false; } for (let resourceType of resourceTypes) { let resourceLabel = this.#resourceTypeList.querySelector( `label[data-resource-type="${resourceType}"]` ); if (resourceLabel) { resourceLabel.hidden = false; resourceLabel.control.checked = true; let labelSpan = resourceLabel.querySelector( "span[default-data-l10n-id]" ); if (labelSpan) { if (MigrationWizardConstants.USES_FAVORITES.includes(key)) { document.l10n.setAttributes( labelSpan, labelSpan.getAttribute("ie-edge-data-l10n-id") ); } else { document.l10n.setAttributes( labelSpan, labelSpan.getAttribute("default-data-l10n-id") ); } } } } let selectAll = this.#shadowRoot.querySelector("#select-all").control; selectAll.checked = true; this.#displaySelectedResources(); this.#browserProfileSelector.selectedPanelItem = panelItem; let selectionPage = this.#shadowRoot.querySelector( "div[name='page-selection']" ); selectionPage.setAttribute("migrator-type", panelItem.getAttribute("type")); selectionPage.toggleAttribute( "no-resources", panelItem.getAttribute("type") == MigrationWizardConstants.MIGRATOR_TYPES.BROWSER && !resourceTypes.length ); } /** * Called when showing the browser/profile selection page of the wizard. * * @param {object} state * The state object passed into setState. The following properties are * used: * @param {string[]} state.migrators An array of source browser names that * can be migrated from. */ #onShowingSelection(state) { this.#ensureSelectionDropdown(); this.#browserProfileSelectorList.textContent = ""; let selectionPage = this.#shadowRoot.querySelector( "div[name='page-selection']" ); let details = this.#shadowRoot.querySelector("details"); selectionPage.toggleAttribute("show-import-all", state.showImportAll); details.open = !state.showImportAll; this.#expandedDetails = false; for (let migrator of state.migrators) { let opt = document.createElement("panel-item"); opt.setAttribute("key", migrator.key); opt.setAttribute("type", migrator.type); opt.profile = migrator.profile; opt.displayName = migrator.displayName; opt.resourceTypes = migrator.resourceTypes; opt.hasPermissions = migrator.hasPermissions; opt.brandImage = migrator.brandImage; // Bug 1823489 - since the panel-list and panel-items are slotted, we // cannot style them directly from migration-wizard.css. We use inline // styles for now to achieve the desired appearance, but bug 1823489 // will investigate having MigrationWizard own the , // and 's so that styling can be done in the // stylesheet instead. let button = opt.shadowRoot.querySelector("button"); button.style.minHeight = "40px"; if (migrator.brandImage) { button.style.backgroundImage = `url(${migrator.brandImage})`; } else { button.style.backgroundImage = `url("chrome://global/skin/icons/defaultFavicon.svg")`; } // Bug 1823489 - since the panel-list and panel-items are slotted, we // cannot style them or their children in migration-wizard.css. We use // inline styles for now to achieve the desired appearance, but bug 1823489 // will investigate having MigrationWizard own the , // and 's so that styling can be done in the // stylesheet instead. if (migrator.type == MigrationWizardConstants.MIGRATOR_TYPES.FILE) { button.style.backgroundSize = "20px"; button.style.backgroundPosition = "6px center"; if (this.#browserProfileSelectorList.isDocumentRTL()) { button.style.backgroundPositionX = "right 6px"; } } if (migrator.profile) { document.l10n.setAttributes( opt, "migration-wizard-selection-option-with-profile", { sourceBrowser: migrator.displayName, profileName: migrator.profile.name, } ); } else { document.l10n.setAttributes( opt, "migration-wizard-selection-option-without-profile", { sourceBrowser: migrator.displayName, } ); } this.#browserProfileSelectorList.appendChild(opt); } if (state.migrators.length) { this.#onBrowserProfileSelectionChanged( this.#browserProfileSelectorList.firstElementChild ); } // Since this is called before the named-deck actually switches to // show the selection page, we cannot focus this button immediately. // Instead, we use a rAF to queue this up for focusing before the // next paint. requestAnimationFrame(() => { this.#browserProfileSelector.focus({ focusVisible: false }); }); } /** * @typedef {object} ProgressState * The migration progress state for a resource. * @property {boolean} inProgress * True if progress is still underway. * @property {string} [message=undefined] * An optional message to display underneath the resource in * the progress dialog. This message is only shown when inProgress * is `false`. */ /** * Called when showing the progress / success page of the wizard. * * @param {object} state * The state object passed into setState. The following properties are * used: * @param {string} state.key * The key of the migrator being used. * @param {Object} state.progress * An object whose keys match one of DISPLAYED_RESOURCE_TYPES. * * Any resource type not included in state.progress will be hidden. */ #onShowingProgress(state) { // Any resource progress group not included in state.progress is hidden. let progressPage = this.#shadowRoot.querySelector( "div[name='page-progress']" ); let resourceGroups = progressPage.querySelectorAll( ".resource-progress-group" ); let totalProgressGroups = Object.keys(state.progress).length; let remainingProgressGroups = totalProgressGroups; for (let group of resourceGroups) { let resourceType = group.dataset.resourceType; if (!state.progress.hasOwnProperty(resourceType)) { group.hidden = true; continue; } group.hidden = false; let progressIcon = group.querySelector(".progress-icon"); let successText = group.querySelector(".success-text"); let labelSpan = group.querySelector("span[default-data-l10n-id]"); if (labelSpan) { if (MigrationWizardConstants.USES_FAVORITES.includes(state.key)) { document.l10n.setAttributes( labelSpan, labelSpan.getAttribute("ie-edge-data-l10n-id") ); } else { document.l10n.setAttributes( labelSpan, labelSpan.getAttribute("default-data-l10n-id") ); } } if (state.progress[resourceType].inProgress) { document.l10n.setAttributes( progressIcon, "migration-wizard-progress-icon-in-progress" ); progressIcon.classList.remove("completed"); successText.textContent = ""; // With no status text, we re-insert the   so that the status // text area does not fully collapse. successText.appendChild(document.createTextNode("\u00A0")); } else { document.l10n.setAttributes( progressIcon, "migration-wizard-progress-icon-completed" ); progressIcon.classList.add("completed"); successText.textContent = state.progress[resourceType].message; remainingProgressGroups--; } } let migrationDone = remainingProgressGroups == 0; let headerL10nID = migrationDone ? "migration-wizard-progress-done-header" : "migration-wizard-progress-header"; let header = this.#shadowRoot.getElementById("progress-header"); document.l10n.setAttributes(header, headerL10nID); let finishButtons = progressPage.querySelectorAll(".finish-button"); let cancelButton = progressPage.querySelector(".cancel-close"); for (let finishButton of finishButtons) { finishButton.hidden = !migrationDone; } cancelButton.hidden = migrationDone; if (migrationDone) { // Since this might be called before the named-deck actually switches to // show the progress page, we cannot focus this button immediately. // Instead, we use a rAF to queue this up for focusing before the // next paint. requestAnimationFrame(() => { let button = this.#dialogMode ? progressPage.querySelector(".done-button") : progressPage.querySelector(".continue-button"); button.focus({ focusVisible: false }); }); } } /** * Called when showing the progress / success page of the wizard for * files. * * @param {object} state * The state object passed into setState. The following properties are * used: * @param {string} state.title * The string to display in the header. * @param {Object} state.progress * An object whose keys match one of DISPLAYED_FILE_RESOURCE_TYPES. * * Any resource type not included in state.progress will be hidden. */ #onShowingFileImportProgress(state) { // Any resource progress group not included in state.progress is hidden. let progressPage = this.#shadowRoot.querySelector( "div[name='page-file-import-progress']" ); let resourceGroups = progressPage.querySelectorAll( ".resource-progress-group" ); let totalProgressGroups = Object.keys(state.progress).length; let remainingProgressGroups = totalProgressGroups; for (let group of resourceGroups) { let resourceType = group.dataset.resourceType; if (!state.progress.hasOwnProperty(resourceType)) { group.hidden = true; continue; } group.hidden = false; let progressIcon = group.querySelector(".progress-icon"); let successText = group.querySelector(".success-text"); if (state.progress[resourceType].inProgress) { document.l10n.setAttributes( progressIcon, "migration-wizard-progress-icon-in-progress" ); progressIcon.classList.remove("completed"); successText.textContent = ""; // With no status text, we re-insert the   so that the status // text area does not fully collapse. successText.appendChild(document.createTextNode("\u00A0")); } else { document.l10n.setAttributes( progressIcon, "migration-wizard-progress-icon-completed" ); progressIcon.classList.add("completed"); successText.textContent = state.progress[resourceType].message; remainingProgressGroups--; } } let migrationDone = remainingProgressGroups == 0; let header = this.#shadowRoot.getElementById("file-import-progress-header"); header.textContent = state.title; let doneButton = progressPage.querySelector(".primary"); let cancelButton = progressPage.querySelector(".cancel-close"); doneButton.hidden = !migrationDone; cancelButton.hidden = migrationDone; if (migrationDone) { // Since this might be called before the named-deck actually switches to // show the progress page, we cannot focus this button immediately. // Instead, we use a rAF to queue this up for focusing before the // next paint. requestAnimationFrame(() => { doneButton.focus({ focusVisible: false }); }); } } /** * Called when showing the "no browsers found" page of the wizard. * * @param {object} state * The state object passed into setState. The following properties are * used: * @param {string} state.hasFileMigrators * True if at least one FileMigrator is available for use. */ #onShowingNoBrowsersFound(state) { this.#chooseImportFromFile.hidden = !state.hasFileMigrators; } /** * Certain parts of the MigrationWizard need to be modified slightly * in order to work properly with Storybook. This method should be called * to apply those changes after changing state. */ #updateForStorybook() { // The CSS mask used for the progress spinner cannot be loaded via // chrome:// URIs in Storybook. We work around this by exposing the // progress elements as custom parts that the MigrationWizard story // can style on its own. this.#shadowRoot.querySelectorAll(".progress-icon").forEach(progressEl => { if (progressEl.classList.contains("completed")) { progressEl.removeAttribute("part"); } else { progressEl.setAttribute("part", "progress-spinner"); } }); } /** * Takes the current state of the selections page and bundles them * up into a MigrationWizard:BeginMigration event that can be handled * externally to perform the actual migration. */ #doImport() { let migrationEventDetail = this.#gatherMigrationEventDetails(); this.dispatchEvent( new CustomEvent("MigrationWizard:BeginMigration", { bubbles: true, detail: migrationEventDetail, }) ); } /** * @typedef {object} MigrationDetails * @property {string} key * The key for a MigratorBase subclass. * @property {object|null} profile * A representation of a browser profile. This is serialized and originally * sent down from the parent via the GetAvailableMigrators message. * @property {string[]} resourceTypes * An array of resource types that the user is attempted to import. These * strings should be from MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES. * @property {boolean} hasPermissions * True if this MigrationWizardChild told us that the associated * MigratorBase subclass for the key has enough permission to read * the requested resources. * @property {boolean} expandedDetails * True if the user clicked on the element to expand the resource * type list. */ /** * Pulls information from the DOM state of the MigrationWizard and constructs * and returns an object that can be used to begin migration via and event * sent to the MigrationWizardChild. * * @returns {MigrationDetails} details */ #gatherMigrationEventDetails() { let panelItem = this.#browserProfileSelector.selectedPanelItem; let key = panelItem.getAttribute("key"); let type = panelItem.getAttribute("type"); let profile = panelItem.profile; let hasPermissions = panelItem.hasPermissions; let resourceTypeFields = this.#resourceTypeList.querySelectorAll( "label[data-resource-type]" ); let resourceTypes = []; for (let resourceTypeField of resourceTypeFields) { if (resourceTypeField.control.checked) { resourceTypes.push(resourceTypeField.dataset.resourceType); } } return { key, type, profile, resourceTypes, hasPermissions, expandedDetails: this.#expandedDetails, }; } /** * Sends a request to gain read access to the Safari profile folder on * macOS, and upon gaining access, performs a migration using the current * settings as gathered by #gatherMigrationEventDetails */ #requestSafariPermissions() { let migrationEventDetail = this.#gatherMigrationEventDetails(); this.dispatchEvent( new CustomEvent("MigrationWizard:RequestSafariPermissions", { bubbles: true, detail: migrationEventDetail, }) ); } /** * Sends a request to get a string path for a passwords file exported * from Safari. */ #selectSafariPasswordFile() { let migrationEventDetail = this.#gatherMigrationEventDetails(); this.dispatchEvent( new CustomEvent("MigrationWizard:SelectSafariPasswordFile", { bubbles: true, detail: migrationEventDetail, }) ); } /** * Changes selected-data-header text and selected-data text based on * how many resources are checked */ async #displaySelectedResources() { let resourceTypeLabels = this.#resourceTypeList.querySelectorAll( "label:not([hidden])[data-resource-type]" ); let panelItem = this.#browserProfileSelector.selectedPanelItem; let key = panelItem.getAttribute("key"); let totalResources = resourceTypeLabels.length; let checkedResources = 0; let selectedData = this.#shadowRoot.querySelector(".selected-data"); let selectedDataArray = []; let resourceTypeToLabelIDs = { [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS]: "migration-list-bookmark-label", [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS]: "migration-list-password-label", [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY]: "migration-list-history-label", [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA]: "migration-list-autofill-label", [MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PAYMENT_METHODS]: "migration-list-payment-methods-label", }; if (MigrationWizardConstants.USES_FAVORITES.includes(key)) { resourceTypeToLabelIDs[ MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS ] = "migration-list-favorites-label"; } let resourceTypes = Object.keys(resourceTypeToLabelIDs); let labelIds = Object.values(resourceTypeToLabelIDs).map(id => { return { id }; }); let labels = await document.l10n.formatValues(labelIds); let resourceTypeLabelMapping = new Map(); for (let i = 0; i < resourceTypes.length; ++i) { let resourceType = resourceTypes[i]; resourceTypeLabelMapping.set(resourceType, labels[i]); } let formatter = new Intl.ListFormat(undefined, { style: "long", type: "conjunction", }); for (let resourceTypeLabel of resourceTypeLabels) { if (resourceTypeLabel.control.checked) { selectedDataArray.push( resourceTypeLabelMapping.get(resourceTypeLabel.dataset.resourceType) ); checkedResources++; } } if (selectedDataArray.length) { selectedDataArray[0] = selectedDataArray[0].charAt(0).toLocaleUpperCase() + selectedDataArray[0].slice(1); selectedData.textContent = formatter.format(selectedDataArray); } else { selectedData.textContent = "\u00A0"; } let selectedDataHeader = this.#shadowRoot.querySelector( ".selected-data-header" ); let importButton = this.#shadowRoot.querySelector("#import"); importButton.disabled = checkedResources == 0; if (checkedResources == 0) { document.l10n.setAttributes( selectedDataHeader, "migration-no-selected-data-label" ); } else if (checkedResources < totalResources) { document.l10n.setAttributes( selectedDataHeader, "migration-selected-data-label" ); } else { document.l10n.setAttributes( selectedDataHeader, "migration-all-available-data-label" ); } let selectionPage = this.#shadowRoot.querySelector( "div[name='page-selection']" ); selectionPage.toggleAttribute("single-item", totalResources == 1); this.dispatchEvent( new CustomEvent("MigrationWizard:ResourcesUpdated", { bubbles: true }) ); } handleEvent(event) { switch (event.type) { case "click": { if ( event.target == this.#importButton || event.target == this.#importFromFileButton ) { this.#doImport(); } else if ( event.target.classList.contains("cancel-close") || event.target.classList.contains("finish-button") ) { this.dispatchEvent( new CustomEvent("MigrationWizard:Close", { bubbles: true }) ); } else if (event.target == this.#browserProfileSelector) { this.#browserProfileSelectorList.show(event); } else if ( event.currentTarget == this.#browserProfileSelectorList && event.target != this.#browserProfileSelectorList ) { this.#onBrowserProfileSelectionChanged(event.target); // If the user selected a file migration type from the selector, we'll // help the user out by immediately starting the file migration flow, // rather than waiting for them to click the "Select File". if ( event.target.getAttribute("type") == MigrationWizardConstants.MIGRATOR_TYPES.FILE ) { this.#doImport(); } } else if (event.target == this.#safariPermissionButton) { this.#requestSafariPermissions(); } else if (event.currentTarget == this.#resourceSummary) { this.#expandedDetails = true; } else if (event.target == this.#chooseImportFromFile) { this.dispatchEvent( new CustomEvent("MigrationWizard:RequestState", { bubbles: true, detail: { allowOnlyFileMigrators: true, }, }) ); } else if (event.target == this.#safariPasswordImportSkipButton) { // If the user chose to skip importing passwords from Safari, we // programmatically uncheck the PASSWORDS resource type and re-request // import. let checkbox = this.#shadowRoot.querySelector( `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]` ).control; checkbox.checked = false; // If there are no other checked checkboxes, go back to the selection // screen. let checked = this.#shadowRoot.querySelectorAll( `label[data-resource-type] > input:checked` ).length; if (!checked) { this.requestState(); } else { this.#doImport(); } } else if (event.target == this.#safariPasswordImportSelectButton) { this.#selectSafariPasswordFile(); } break; } case "change": { if (event.target == this.#browserProfileSelector) { this.#onBrowserProfileSelectionChanged(); } else if (event.target == this.#selectAllCheckbox) { let checkboxes = this.#shadowRoot.querySelectorAll( 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]' ); for (let checkbox of checkboxes) { checkbox.checked = this.#selectAllCheckbox.checked; } this.#displaySelectedResources(); } else { let checkboxes = this.#shadowRoot.querySelectorAll( 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]' ); let allVisibleChecked = Array.from(checkboxes).every(checkbox => { return checkbox.checked; }); this.#selectAllCheckbox.checked = allVisibleChecked; this.#displaySelectedResources(); } break; } } } } if (globalThis.customElements) { customElements.define("migration-wizard", MigrationWizard); }