UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

191 lines (157 loc) 6.55 kB
/* * Copyright 2018 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. */ import {EventDispatcher} from 'three'; import ModelViewerElementBase from '../../model-viewer-base.js'; import {debounce, getFirstMapKey} from '../../utilities.js'; export const INITIAL_STATUS_ANNOUNCEMENT = 'This page includes one or more 3D models that are loading'; export const FINISHED_LOADING_ANNOUNCEMENT = 'All 3D models in the page have loaded'; const UPDATE_STATUS_DEBOUNCE_MS = 100; const $modelViewerStatusInstance = Symbol('modelViewerStatusInstance'); const $updateStatus = Symbol('updateStatus'); interface InstanceLoadingStatus { onUnregistered: () => void; } /** * The LoadingStatusAnnouncer manages announcements of loading status across * all <model-viewer> elements in the document at any given time. As new * <model-viewer> elements are connected to the document, they are registered * with a LoadingStatusAnnouncer singleton. As they are disconnected, the are * also unregistered. Announcements are made to indicate the following * conditions: * * 1. There are <model-viewer> elements that have yet to finish loading * 2. All <model-viewer> elements in the page have finished attempting to load */ export class LoadingStatusAnnouncer extends EventDispatcher { /** * The "status" instance is the <model-viewer> instance currently designated * to announce the loading status of all <model-viewer> elements in the * document at any given time. It might change as <model-viewer> elements are * attached or detached over time. */ protected[$modelViewerStatusInstance]: ModelViewerElementBase|null = null; protected registeredInstanceStatuses: Map<ModelViewerElementBase, InstanceLoadingStatus> = new Map<ModelViewerElementBase, InstanceLoadingStatus>(); protected loadingPromises: Array<Promise<any>> = []; /** * This element is a node that floats around the document as the status * instance changes (see above). It is a singleton that represents the loading * status for all <model-viewer> elements currently in the page. It has its * role attribute set to "status", which causes screen readers to announce * any changes to its text content. * * @see https://www.w3.org/TR/wai-aria-1.1/#status */ readonly statusElement: HTMLParagraphElement = document.createElement('p'); protected statusUpdateInProgress: boolean = false; protected[$updateStatus]: () => void = debounce(() => this.updateStatus(), UPDATE_STATUS_DEBOUNCE_MS); constructor() { super(); const {statusElement} = this; const {style} = statusElement; statusElement.setAttribute('role', 'status'); style.position = 'absolute'; style.color = 'transparent'; style.top = style.left = style.margin = '0'; style.pointerEvents = 'none'; } /** * Register a <model-viewer> element with the announcer. If it is not yet * loaded, its loading status will be tracked by the announcer. */ registerInstance(modelViewer: ModelViewerElementBase) { if (this.registeredInstanceStatuses.has(modelViewer)) { return; } let onUnregistered = () => {}; const loadShouldBeMeasured = modelViewer.loaded === false && !!(modelViewer as any).src; const loadAttemptCompletes = new Promise((resolve) => { if (!loadShouldBeMeasured) { resolve(); return; } const resolveHandler = () => { resolve(); modelViewer.removeEventListener('load', resolveHandler); modelViewer.removeEventListener('error', resolveHandler); }; modelViewer.addEventListener('load', resolveHandler); modelViewer.addEventListener('error', resolveHandler); onUnregistered = resolveHandler; }); this.registeredInstanceStatuses.set(modelViewer, {onUnregistered}); this.loadingPromises.push(loadAttemptCompletes); if (this.modelViewerStatusInstance == null) { this.modelViewerStatusInstance = modelViewer; } } /** * Unregister a <model-viewer> element with the announcer. Its loading status * will no longer be tracked by the announcer. */ unregisterInstance(modelViewer: ModelViewerElementBase) { if (!this.registeredInstanceStatuses.has(modelViewer)) { return; } const statuses = this.registeredInstanceStatuses; const instanceStatus = statuses.get(modelViewer)!; statuses.delete(modelViewer); instanceStatus.onUnregistered(); if (this.modelViewerStatusInstance === modelViewer) { this.modelViewerStatusInstance = statuses.size > 0 ? getFirstMapKey<ModelViewerElementBase, InstanceLoadingStatus>( statuses) : null; } } protected get modelViewerStatusInstance(): ModelViewerElementBase|null { return this[$modelViewerStatusInstance]; } protected set modelViewerStatusInstance(value: ModelViewerElementBase|null) { const currentInstance = this[$modelViewerStatusInstance]; if (currentInstance === value) { return; } const {statusElement} = this; if (value != null && value.shadowRoot != null) { value.shadowRoot.appendChild(statusElement); } else if (statusElement.parentNode != null) { statusElement.parentNode.removeChild(statusElement); } this[$modelViewerStatusInstance] = value; this[$updateStatus](); } protected async updateStatus() { if (this.statusUpdateInProgress || this.loadingPromises.length === 0) { return; } this.statusElement.textContent = INITIAL_STATUS_ANNOUNCEMENT; this.statusUpdateInProgress = true; this.dispatchEvent({type: 'initial-status-announced'}); while (this.loadingPromises.length) { const {loadingPromises} = this; this.loadingPromises = []; await Promise.all(loadingPromises); } this.statusElement.textContent = FINISHED_LOADING_ANNOUNCEMENT; this.statusUpdateInProgress = false; this.dispatchEvent({type: 'finished-loading-announced'}); } }