UNPKG

@googleworkspace/drive-picker-element

Version:
355 lines (309 loc) 13.5 kB
/** * Copyright 2024 Google LLC * * 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 { getBoolAttr, getNumberAttribute, loadApi, requestAccessToken, setBoolAttrWithDefault, } from "../utils"; type View = google.picker.DocsView; interface DrivePickerDocsViewElement extends HTMLElement { view: google.picker.DocsView; } declare global { interface GlobalEventHandlersEventMap { /** @deprecated - Use "picker:oauth:response" */ "picker:authenticated": CustomEvent<{ token: string }>; "picker:oauth:error": CustomEvent< | google.accounts.oauth2.ClientConfigError | google.accounts.oauth2.TokenResponse >; "picker:oauth:response": CustomEvent<google.accounts.oauth2.TokenResponse>; "picker:canceled": CustomEvent<google.picker.ResponseObject>; "picker:picked": CustomEvent<google.picker.ResponseObject>; "picker:error": CustomEvent<unknown>; } } /** * The `drive-picker` web component provides a convenient way to declaratively * build * [`google.picker.Picker`](https://developers.google.com/drive/picker/reference/picker) * by using the component attributes mapped to the corresponding methods of * [`google.picker.PickerBuilder`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder). * * @element drive-picker * * @fires {google.picker.ResponseObject} picker:canceled - Triggered when the user cancels the picker dialog. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). * @fires {google.picker.ResponseObject} picker:picked - Triggered when the user picks one or more items. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). * @fires {google.picker.ResponseObject} picker:error - Triggered when an error occurs. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). * @fires {google.accounts.oauth2.ClientConfigError|google.accounts.oauth2.TokenResponse} picker:oauth:error - Triggered when an error occurs in the OAuth flow. See the [error guide](https://developers.google.com/identity/oauth2/web/guides/error). Note that the `TokenResponse` object can have error fields. * @fires {google.accounts.oauth2.TokenResponse} picker:oauth:response - Triggered when an OAuth flow completes. See the [token model guide](https://developers.google.com/identity/oauth2/web/guides/use-token-model). * * @slot - The default slot contains View elements to display in the picker. * Each View element should implement a property `view` of type * `google.picker.View`. * @attr {string} app-id - The Google Drive app ID. See [`PickerBuilder.setAppId`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setappid). * @attr {string} client-id - The OAuth 2.0 client ID. See [Using OAuth 2.0 to Access Google APIs](https://developers.google.com/identity/protocols/oauth2). * @attr {string} developer-key - The API key for accessing Google Picker API. See [`PickerBuilder.setDeveloperKey`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setdeveloperkey). * @attr {"default"|"true"|"false"} hide-title-bar - Hides the title bar of the * picker if set to true. See [`PickerBuilder.hideTitleBar`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.hidetitlebar). * @attr {string} locale - The locale to use for the picker. See [`PickerBuilder.setLocale`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setlocale). * @attr {number} max-items - The maximum number of items that can be selected. See [`PickerBuilder.setMaxItems`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setmaxitems). * @attr {boolean} mine-only - If set to true, only shows files owned by the * user. See [`PickerBuilder.enableFeature`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.enablefeature). * @attr {boolean} multiselect - Enables multiple file selection if set to true. See [`PickerBuilder.enableFeature`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.enablefeature). * @attr {boolean} nav-hidden - Hides the navigation pane if set to true. See [`PickerBuilder.enableFeature`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.enablefeature). * @attr {string} oauth-token - The OAuth 2.0 token for authentication. See [`PickerBuilder.setOAuthToken`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setoauthtoken). * @attr {string} origin - The origin parameter for the picker. See [`PickerBuilder.setOrigin`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setorigin). * @attr {string} relay-url - The relay URL for the picker. See [`PickerBuilder.setRelayUrl`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.setrelayurl). * @attr {string} scope - The OAuth 2.0 scope for the picker. The default is `https://www.googleapis.com/auth/drive.file`. See [Drive API scopes](https://developers.google.com/drive/api/guides/api-specific-auth#drive-scopes). * @attr {string} title - The title of the picker. See [`PickerBuilder.setTitle`](https://developers.google.com/drive/picker/reference/picker.pickerbuilder.settitle). * @attr {string} hd - The hosted domain to restrict sign-in to. (Optional) See the `hd` field in the OpenID Connect docs. * @attr {boolean} include-granted-scopes - Enables applications to use incremental authorization. See [`TokenClientConfig.include_granted_scopes`](https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClientConfig). * @attr {string} login-hint - An email address or an ID token 'sub' value. Google will use the value as a hint of which user to sign in. See the `login_hint` field in the OpenID Connect docs. * @attr {""|"none"|"consent"|"select_account"} prompt - A space-delimited, case-sensitive list of prompts to present the user. See [`TokenClientConfig.prompt`](https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClientConfig) * * @example * *```html *<drive-picker * app-id="246724281745" * client-id="246724281745-v9ouai8ood5o69r3ug29aaqeqflomijd.apps.googleusercontent.com" *> * <drive-picker-docs-view></drive-picker-docs-view> *</drive-picker> *``` * */ export class DrivePickerElement extends HTMLElement { static get observedAttributes() { return [ "app-id", "client-id", "developer-key", "hide-title-bar", "locale", "max-items", "mine-only", "multiselect", "nav-hidden", "oauth-token", "origin", "relay-url", "scope", "title", ]; } private picker: google.picker.Picker | undefined; private observer: MutationObserver | undefined; private google: typeof google | undefined; private loading: Promise<void> | undefined; /** * The visibility of the picker. */ public get visible(): boolean { return Boolean(this.picker?.isVisible()); } /** * Controls the visibility of the picker after the picker dialog has been * closed. If any of the attributes change, the picker will be rebuilt and * the visibility will be reset. */ set visible(value: boolean) { this.picker?.setVisible(value); } public get tokenClientConfig(): Omit< google.accounts.oauth2.TokenClientConfig, "callback" | "error_callback" > { const clientId = this.getAttribute("client-id"); const scope = this.getAttribute("scope") ?? "https://www.googleapis.com/auth/drive.file"; if (!clientId || !scope) { throw new Error("client-id and scope are required attributes"); } return { client_id: clientId, hd: this.getAttribute("hd") ?? undefined, include_granted_scopes: Boolean( this.getAttribute("include-granted-scope"), ), login_hint: this.getAttribute("login-hint") ?? undefined, prompt: (this.getAttribute("prompt") ?? "") as google.accounts.oauth2.TokenClientConfig["prompt"], scope, }; } attributeChangedCallback() { this.build(); return; } private async build() { this.picker?.dispose(); // this await is necessary as an attribute may have changed // prior to the API initially being loaded await this.loading; if (!this.google) return; let builder = new this.google.picker.PickerBuilder().setCallback( (data: google.picker.ResponseObject) => { this.callbackToDispatchEvent(data); }, ); const appId = this.getAttribute("app-id"); if (appId !== null) builder = builder.setAppId(appId); const developerKey = this.getAttribute("developer-key"); if (developerKey !== null) builder = builder.setDeveloperKey(developerKey); const locale = this.getAttribute("locale"); if (locale !== null) builder = builder.setLocale(locale as google.picker.Locales); const maxItems = getNumberAttribute(this, "max-items"); if (maxItems !== null) builder = builder.setMaxItems(maxItems); const origin = this.getAttribute("origin"); if (origin !== null) builder = builder.setOrigin(origin); const relayUrl = this.getAttribute("relay-url"); if (relayUrl !== null) builder = builder.setRelayUrl(relayUrl); const title = this.getAttribute("title"); if (title !== null) builder = builder.setTitle(title); setBoolAttrWithDefault( "hide-title-bar", this, builder.hideTitleBar, builder, ); // OAuth token is required either as an attribute or from the OAuth flow using the client ID and scope const oauthToken = this.getAttribute("oauth-token") ?? (await this.requestAccessToken()); if (!oauthToken) return; // biome-ignore lint/style/noNonNullAssertion: <explanation> builder = builder.setOAuthToken(oauthToken!); if (getBoolAttr(this, "multiselect")) { builder = builder.enableFeature( this.google.picker.Feature.MULTISELECT_ENABLED, ); } if (getBoolAttr(this, "mine-only")) { builder = builder.enableFeature(this.google.picker.Feature.MINE_ONLY); } if (getBoolAttr(this, "nav-hidden")) { builder = builder.enableFeature(this.google.picker.Feature.NAV_HIDDEN); } for (const view of this.views) { builder = builder.addView(view); } this.picker = builder.build(); this.picker.setVisible(true); } /** * The `google.Picker.View` objects to display in the picker as defined by the slot elements. */ private get views(): (View | google.picker.ViewId)[] { const views = nestedViews(this); return views.length ? views : ["all" as google.picker.ViewId]; } async connectedCallback(): Promise<void> { this.loading = loadApi().then((google) => { this.google = google; this.build(); }); // Watch for changes in the picker element slot and their attributes this.observer = new MutationObserver((mutations) => { const filteredMutations = mutations.filter( (mutation) => mutation.type === "childList" || (mutation.type === "attributes" && mutation.target !== this), ); if (filteredMutations.length) { this.build(); } }); this.observer?.observe(this, { childList: true, subtree: true, attributes: true, }); } private callbackToDispatchEvent(detail: google.picker.ResponseObject) { let eventType: keyof GlobalEventHandlersEventMap; switch (detail.action) { case google.picker.Action.CANCEL: eventType = "picker:canceled"; break; case google.picker.Action.PICKED: eventType = "picker:picked"; break; case google.picker.Action.ERROR: eventType = "picker:error"; break; default: return; } this.dispatchEvent( new CustomEvent(eventType, { detail, }), ); } private async requestAccessToken(): Promise<string | undefined> { return requestAccessToken(this.tokenClientConfig) .then((response) => { const { access_token: token } = response; if (!token) { this.dispatchEvent( new CustomEvent("picker:oauth:error", { detail: response, }), ); return undefined; } // TODO - remove deprecated event this.dispatchEvent( new CustomEvent("picker:authenticated", { detail: { token } }), ); this.dispatchEvent( new CustomEvent("picker:oauth:response", { detail: response }), ); return token; }) .catch((error) => { this.dispatchEvent( new CustomEvent("picker:oauth:error", { detail: error, }), ); return undefined; }); } disconnectedCallback(): void { this.picker?.dispose(); } } function isView(obj: HTMLElement): obj is DrivePickerDocsViewElement { return "view" in obj && obj.view instanceof window.google.picker.View; } function filterElementsToViewOrViewGroup( elements: Array<HTMLElement>, ): Array<View> { return elements .filter((element) => isView(element)) .map((element) => element.view); } function nestedViews(target: HTMLElement, selector = "*"): Array<View> { return filterElementsToViewOrViewGroup( Array.from(target.querySelectorAll<HTMLElement>(selector)), ); }