// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as ACData from "adaptivecards-templating"; import * as Adaptive from "adaptivecards"; import { CatalogueEntry, SampleCatalogue } from "./catalogue"; import { Dialog } from "./dialog"; import { Constants } from "adaptivecards-controls"; export interface CardData { cardPayload?: string sampleData?: string, sampleHostData?: string, thumbnail?: HTMLElement | (() => HTMLElement) } type CardDataCallback = (output: CardData) => any; type CardDataProvider = (callback?: CardDataCallback) => CardData | void; interface OpenSampleItemProps { label: string, onClick?: (ev: MouseEvent) => any, onKeyEnterEvent?: (ev: KeyboardEvent) => any, cardData?: CardData | CardDataProvider, } class OpenSampleItem { onComplete: CardDataCallback; onNavigateToNextList?: () => void; onNavigateToPreviousList?: () => void; constructor(readonly props: OpenSampleItemProps) { } private static _id = 0; private static getNewItemId(prefix: string): string { const newId = prefix + "-" + OpenSampleItem._id; OpenSampleItem._id++; return newId; } render(): HTMLElement { const newItemId = OpenSampleItem.getNewItemId("acd-open-sample-item-title"); const element = document.createElement("div"); element.className = "acd-open-sample-item"; element.tabIndex = 0; element.setAttribute("aria-labelledBy", newItemId); element.setAttribute("role", "listitem"); element.onclick = this.props.onClick ?? ( (e) => { this.onCardSelected(); }) element.onkeyup = (e: KeyboardEvent) => { switch (e.key) { case Constants.keys.enter: // If onKeyEnterEvent has been specified, we will follow that logic for selecting an OpenSampleItem if (this.props.onKeyEnterEvent) { this.props.onKeyEnterEvent(e); } else { // If onKeyEnterEvent is not specified, we assume OpenSampleItem should be selected and loaded as usual this.onCardSelected(); } break; case Constants.keys.down: this.navigateToNextElement(element); break; case Constants.keys.up: this.navigateToPreviousElement(element); break; case Constants.keys.right: this.navigateToNextElement(element); break; case Constants.keys.left: this.navigateToPreviousElement(element); break; default: break; } } const thumbnailHost = document.createElement("div"); thumbnailHost.className = "acd-open-sample-item-thumbnail"; if (this.props.cardData instanceof Function) { const spinner = document.createElement("div"); spinner.className = "acd-spinner"; spinner.style.width = "28px"; spinner.style.height = "28px"; thumbnailHost.appendChild(spinner); const cardData = this.props.cardData((cardData: CardData) => { thumbnailHost.removeChild(spinner); if (cardData.thumbnail instanceof Function) { thumbnailHost.appendChild(cardData.thumbnail()) } else if (cardData.thumbnail) { thumbnailHost.appendChild(cardData.thumbnail) } }) if (cardData) { thumbnailHost.removeChild(spinner); if (cardData.thumbnail instanceof Function) { thumbnailHost.appendChild(cardData.thumbnail()) } else if (cardData.thumbnail) { thumbnailHost.appendChild(cardData.thumbnail) } } } else if (this.props.cardData) { if (this.props.cardData.thumbnail instanceof Function) { thumbnailHost.appendChild(this.props.cardData.thumbnail()) } else if (this.props.cardData.thumbnail) { thumbnailHost.appendChild(this.props.cardData.thumbnail) } } const displayNameElement = document.createElement("div"); displayNameElement.className = "acd-open-sample-item-title"; displayNameElement.id = newItemId; displayNameElement.innerText = this.props.label; element.appendChild(thumbnailHost); element.appendChild(displayNameElement); return element; } navigateToNextElement(element: HTMLElement) { if (element.nextSibling) { (element.nextSibling as HTMLElement).focus(); } else { if (this.onNavigateToNextList) { this.onNavigateToNextList(); } } } navigateToPreviousElement(element: HTMLElement) { if (element.previousSibling) { (element.previousSibling as HTMLElement).focus(); } else { if (this.onNavigateToPreviousList) { this.onNavigateToPreviousList(); } } } onCardSelected() { if (this.onComplete) { if (this.props.cardData instanceof Function) { const cardData = this.props.cardData(this.onComplete); if (cardData) { this.onComplete(cardData); } } else if (this.props.cardData) { this.onComplete(this.props.cardData); } } } } export interface OpenSampleDialogProps { handlers?: (OpenSampleItemProps | null)[], catalogue?: SampleCatalogue, } export class OpenSampleDialog extends Dialog { private _listElements: HTMLElement[] = []; private _output: CardData; private static _builtinItems: OpenSampleItemProps[] = [ { label: "Blank Card", cardData: { cardPayload: JSON.stringify( { type: "AdaptiveCard", $schema: "http://adaptivecards.io/schemas/adaptive-card.json", version: "1.0", body: [ ] } ), }, }, ]; constructor(readonly props: OpenSampleDialogProps) { super(); } private renderSection(title: string | null, items: (OpenSampleItemProps | null)[]): HTMLElement { const renderedElement = document.createElement("div"); if (title) { const titleElement = document.createElement("div"); titleElement.className = "acd-dialog-title"; titleElement.innerText = title; titleElement.style.marginTop = "10px"; renderedElement.appendChild(titleElement); } const listElement = document.createElement("div"); listElement.className = "acd-open-sample-item-container"; listElement.setAttribute("role", "list"); this._listElements.push(listElement); for (const item of items) { if (!item) continue; const itemElement = new OpenSampleItem(item); itemElement.onComplete = (output: CardData) => { this._output = output; this.close(); } itemElement.onNavigateToNextList = () => { // Find the index of the current list const currentIndex = this._listElements.indexOf(listElement); // If the next index has a valid list, we want to navigate to the first element if ((currentIndex !== -1) && (currentIndex + 1 < this._listElements.length) && this._listElements.at(currentIndex + 1)?.firstChild) { (this._listElements.at(currentIndex + 1).firstChild as HTMLElement).focus(); } } itemElement.onNavigateToPreviousList = () => { // Find the index of the current list const currentIndex = this._listElements.indexOf(listElement); // If the previous index has a valid list, we want to navigate to the last element if ((currentIndex !== -1) && (currentIndex - 1 >= 0) && this._listElements.at(currentIndex - 1)?.lastChild) { (this._listElements.at(currentIndex - 1).lastChild as HTMLElement).focus(); } } listElement.appendChild(itemElement.render()); } renderedElement.appendChild(listElement); return renderedElement; } protected renderContent(): HTMLElement { const renderedElement = document.createElement("div"); renderedElement.style.overflow = "auto"; const featuredSection = this.renderSection(null, OpenSampleDialog._builtinItems.concat(this.props.handlers)) renderedElement.appendChild(featuredSection); if (this.props.catalogue) { const divider = document.createElement("hr"); renderedElement.appendChild(divider); this.props.catalogue.onDownloaded = (sender: SampleCatalogue) => { if (sender.isDownloaded) { const catalogueSection = this.renderSection("Explore", this.props.catalogue.entries.map((entry: CatalogueEntry) => { return { label: entry.displayName, cardData: (callback) => { entry.onDownloaded = (sender: CatalogueEntry) => { let success: boolean = sender.cardPayloadDownloaded; if (success) { try { const cardPayload = JSON.parse(sender.cardPayload); const card = new Adaptive.AdaptiveCard(); const cardData = sender.sampleData ? new ACData.Template(cardPayload).expand( { $root: JSON.parse(sender.sampleData) } ) : cardPayload ; card.parse(cardData); card.render(); card.renderedElement.style.width = "100%"; return callback({ cardPayload: entry.cardPayload, sampleData: entry.sampleData, sampleHostData: "{}", thumbnail: card.renderedElement, }); } catch (e) { // Swallow the exception console.error("Unable to load card sample. Error: " + e); success = false; } } }; entry.download(); }, } }) ); renderedElement.appendChild(catalogueSection); } else { console.error("Sender is not downloaded"); } }; this.props.catalogue.download(); } const firstChild = featuredSection.firstElementChild as HTMLElement; firstChild.focus(); return renderedElement; } get output(): CardData { return this._output; } }