// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as Adaptive from "adaptivecards"; import { AdaptiveCard, CardElement, Carousel} from "adaptivecards"; import { Constants } from "adaptivecards-controls"; import { DraggableElement } from "./draggable-element"; import { IPoint, Utils } from "./miscellaneous"; import * as DesignerPeers from "./designer-peers"; import * as ACData from "adaptivecards-templating"; import * as Shared from "./shared"; import { HostContainer } from "./containers"; import { FieldDefinition } from "./data"; import { Strings } from "./strings"; export enum BindingPreviewMode { NoPreview, GeneratedData, SampleData } export type CardElementType = { new(): Adaptive.CardElement }; export type ActionType = { new(): Adaptive.Action }; export type CardElementPeerType = { new( parent: DesignerPeers.DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeers.DesignerPeerRegistrationBase, cardElement: Adaptive.CardElement, initializeCardElement?: boolean ): DesignerPeers.CardElementPeer }; export type ActionPeerType = { new( parent: DesignerPeers.DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeers.DesignerPeerRegistrationBase, action: Adaptive.Action ): DesignerPeers.ActionPeer }; class DesignerPeerCategory { static Unknown = "Unknown"; static Containers = "Containers"; static Elements = "Elements"; static Inputs = "Inputs"; static Actions = "Actions"; } export abstract class DesignerPeerRegistry { protected _items: Array> = []; readonly defaultRegistration = new DesignerPeers.DesignerPeerRegistrationBase(DesignerPeerCategory.Unknown, "acd-icon-unknownPeer"); constructor() { this.reset(); } abstract reset(); clear() { this._items = []; } findTypeRegistration(sourceType: TSource): DesignerPeers.DesignerPeerRegistration { for (let i = 0; i < this._items.length; i++) { if (this._items[i].sourceType === sourceType) { return this._items[i]; } } return null; } registerPeer(sourceType: TSource, peerType: TPeer, category: string, iconClass: string = null) { var registrationInfo = this.findTypeRegistration(sourceType); if (registrationInfo != null) { registrationInfo.peerType = peerType; } else { registrationInfo = new DesignerPeers.DesignerPeerRegistration ( sourceType, peerType, category, iconClass); this._items.push(registrationInfo); } } unregisterPeer(sourceType: TSource) { for (let i = 0; i < this._items.length; i++) { if (this._items[i].sourceType === sourceType) { this._items.splice(i, 1); return; } } } } export class CardElementPeerRegistry extends DesignerPeerRegistry { reset() { this.clear(); this.registerPeer(Adaptive.AdaptiveCard, DesignerPeers.AdaptiveCardPeer, DesignerPeerCategory.Containers, "acd-icon-adaptiveCard"); this.registerPeer(Adaptive.Container, DesignerPeers.ContainerPeer, DesignerPeerCategory.Containers, "acd-icon-container"); this.registerPeer(Adaptive.ColumnSet, DesignerPeers.ColumnSetPeer, DesignerPeerCategory.Containers, "acd-icon-columnSet"); this.registerPeer(Adaptive.Column, DesignerPeers.ColumnPeer, DesignerPeerCategory.Containers, "acd-icon-column"); this.registerPeer(Adaptive.ImageSet, DesignerPeers.ImageSetPeer, DesignerPeerCategory.Containers, "acd-icon-imageSet"); this.registerPeer(Adaptive.FactSet, DesignerPeers.FactSetPeer, DesignerPeerCategory.Containers, "acd-icon-factSet"); this.registerPeer(Adaptive.Table, DesignerPeers.TablePeer, DesignerPeerCategory.Containers, "acd-icon-table"); this.registerPeer(Adaptive.TableRow, DesignerPeers.TableRowPeer, DesignerPeerCategory.Containers, "acd-icon-tableRow"); this.registerPeer(Adaptive.TableCell, DesignerPeers.TableCellPeer, DesignerPeerCategory.Containers, "acd-icon-tableCell"); this.registerPeer(Adaptive.Carousel, DesignerPeers.CarouselPeer, DesignerPeerCategory.Containers, "acd-icon-carousel"); this.registerPeer(Adaptive.CarouselPage, DesignerPeers.CarouselPagePeer, DesignerPeerCategory.Containers, "acd-icon-carouselPage"); this.registerPeer(Adaptive.TextBlock, DesignerPeers.TextBlockPeer, DesignerPeerCategory.Elements, "acd-icon-textBlock"); this.registerPeer(Adaptive.RichTextBlock, DesignerPeers.RichTextBlockPeer, DesignerPeerCategory.Elements, "acd-icon-richTextBlock"); this.registerPeer(Adaptive.Image, DesignerPeers.ImagePeer, DesignerPeerCategory.Elements, "acd-icon-image"); this.registerPeer(Adaptive.Media, DesignerPeers.MediaPeer, DesignerPeerCategory.Elements, "acd-icon-media"); this.registerPeer(Adaptive.ActionSet, DesignerPeers.ActionSetPeer, DesignerPeerCategory.Elements, "acd-icon-actionSet"); this.registerPeer(Adaptive.TextInput, DesignerPeers.TextInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputText"); this.registerPeer(Adaptive.DateInput, DesignerPeers.DateInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputDate"); this.registerPeer(Adaptive.TimeInput, DesignerPeers.TimeInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputTime"); this.registerPeer(Adaptive.ToggleInput, DesignerPeers.ToggleInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputToggle"); this.registerPeer(Adaptive.NumberInput, DesignerPeers.NumberInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputNumber"); this.registerPeer(Adaptive.ChoiceSetInput, DesignerPeers.ChoiceSetInputPeer, DesignerPeerCategory.Inputs, "acd-icon-inputChoiceSet"); } createPeerInstance(designerSurface: CardDesignerSurface, parent: DesignerPeers.DesignerPeer, cardElement: Adaptive.CardElement, initializeCardElement?: boolean): DesignerPeers.CardElementPeer { var registrationInfo = this.findTypeRegistration((cardElement).constructor); var peer = registrationInfo ? new registrationInfo.peerType(parent, designerSurface, registrationInfo, cardElement, initializeCardElement) : new DesignerPeers.CardElementPeer(parent, designerSurface, this.defaultRegistration, cardElement, initializeCardElement); return peer; } } export class ActionPeerRegistry extends DesignerPeerRegistry { reset() { this.clear(); this.registerPeer(Adaptive.HttpAction, DesignerPeers.HttpActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionHttp"); this.registerPeer(Adaptive.SubmitAction, DesignerPeers.SubmitActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionSubmit"); this.registerPeer(Adaptive.ExecuteAction, DesignerPeers.ExecuteActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionSubmit"); this.registerPeer(Adaptive.OpenUrlAction, DesignerPeers.OpenUrlActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionOpenUrl"); this.registerPeer(Adaptive.ShowCardAction, DesignerPeers.ShowCardActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionShowCard"); this.registerPeer(Adaptive.ToggleVisibilityAction, DesignerPeers.ToggleVisibilityActionPeer, DesignerPeerCategory.Actions, "acd-icon-actionToggleVisibility"); } createPeerInstance(designerSurface: CardDesignerSurface, parent: DesignerPeers.DesignerPeer, action: Adaptive.Action): DesignerPeers.ActionPeer { var registrationInfo = this.findTypeRegistration((action).constructor); var peer = registrationInfo ? new registrationInfo.peerType(parent, designerSurface, registrationInfo, action) : new DesignerPeers.ActionPeer(parent, designerSurface, this.defaultRegistration, action); return peer; } } class DragHandle extends DraggableElement { protected internalRender(): HTMLElement { let element = document.createElement("div"); element.classList.add("acd-peerButton", "acd-peerButton-icon", "fixedWidth", "circular", "acd-icon-drag"); element.title = "Drag to move this element"; element.setAttribute("aria-label", "Drag to move this element"); element.setAttribute("role", "button"); element.style.visibility = "hidden"; element.style.position = "absolute"; element.style.zIndex = "500"; element.tabIndex = 0; return element; } } export abstract class DesignContext { abstract get hostContainer(): HostContainer; abstract get targetVersion(): Adaptive.Version; abstract get dataStructure(): FieldDefinition; abstract get hostDataStructure(): FieldDefinition; abstract get bindingPreviewMode(): BindingPreviewMode; abstract get sampleData(): any; abstract get sampleHostData(): any; } export class CardDesignerSurface { static readonly cardElementPeerRegistry: CardElementPeerRegistry = new CardElementPeerRegistry(); static readonly actionPeerRegistry: ActionPeerRegistry = new ActionPeerRegistry(); private _updateCount: number = 0; private _card: Adaptive.AdaptiveCard; private _allPeers: Array = []; private _rootPeer: DesignerPeers.DesignerPeer; private _cardHost: HTMLElement; private _designerSurface: HTMLDivElement; private _selectedPeer: DesignerPeers.DesignerPeer; private _draggedPeer: DesignerPeers.DesignerPeer; private _dropTarget: DesignerPeers.DesignerPeer; private _dragHandle: DragHandle; private _removeCommandElement: HTMLElement; private _peerCommandsHostElement: HTMLElement; private _serializationContext: Adaptive.SerializationContext; private _isPreviewMode: boolean = false; private _dragVisual?: HTMLElement; private _shouldPersistSelectedElement = false; private _persistentSelectedPeer: DesignerPeers.DesignerPeer; private _persistentSelectedCardElement: CardElement; private updatePeerCommandsLayout() { if (this._selectedPeer) { let peerRect = this._selectedPeer.getBoundingRect(); let dragHandleRect = this._dragHandle.renderedElement.getBoundingClientRect(); let removeButtonRect = this._removeCommandElement.getBoundingClientRect(); this._dragHandle.renderedElement.style.left = (peerRect.left - dragHandleRect.width) + "px"; this._dragHandle.renderedElement.style.top = (peerRect.top - dragHandleRect.height) + "px"; this._removeCommandElement.style.left = peerRect.right + "px"; this._removeCommandElement.style.top = (peerRect.top - removeButtonRect.height) + "px"; this._peerCommandsHostElement.style.left = peerRect.left + "px"; this._peerCommandsHostElement.style.top = (peerRect.bottom + 2) + "px"; this._peerCommandsHostElement.style.width = peerRect.width + "px"; this._dragHandle.renderedElement.style.visibility = this._selectedPeer.isDraggable() ? "visible" : "hidden"; this._removeCommandElement.style.visibility = this._selectedPeer.canBeRemoved() ? "visible" : "hidden"; this._peerCommandsHostElement.style.visibility = this._peerCommandsHostElement.childElementCount > 0 ? "visible" : "hidden"; // Remove from tree if (this._designerSurface.contains(this._dragHandle.renderedElement)) { this._designerSurface.removeChild(this._dragHandle.renderedElement); } if (this._designerSurface.contains(this._removeCommandElement)) { this._designerSurface.removeChild(this._removeCommandElement); } if (this._designerSurface.contains(this._peerCommandsHostElement)) { this._designerSurface.removeChild(this._peerCommandsHostElement); } // Insert to the correct location this._selectedPeer.renderedElement.after(this._dragHandle.renderedElement); this._dragHandle.renderedElement.after(this._removeCommandElement); this._removeCommandElement.after(this._peerCommandsHostElement); } else { this._dragHandle.renderedElement.style.visibility = "hidden"; this._removeCommandElement.style.visibility = "hidden"; this._peerCommandsHostElement.style.visibility = "hidden"; } } private setSelectedPeer(value: DesignerPeers.DesignerPeer) { if (this._selectedPeer != value) { if (this._selectedPeer) { this._selectedPeer.isSelected = false; } this._selectedPeer = value; this._peerCommandsHostElement.innerHTML = ""; if (this._selectedPeer) { this._selectedPeer.isSelected = true; let commands = this._selectedPeer.getCommands(this.context); for (let command of commands) { this._peerCommandsHostElement.appendChild(command.render()); } } this.updatePeerCommandsLayout(); if (this.onSelectedPeerChanged) { this.onSelectedPeerChanged(this._selectedPeer); } } } private peerChanged(peer: DesignerPeers.DesignerPeer, updatePropertySheet: boolean) { this.renderCard(); this.updateLayout(); if (updatePropertySheet && this.onSelectedPeerChanged) { this.onSelectedPeerChanged(this._selectedPeer); } } private peerRemoved(peer: DesignerPeers.DesignerPeer) { this._allPeers.splice(this._allPeers.indexOf(peer), 1); if (peer == this._selectedPeer) { this.setSelectedPeer(null); } if (this._updateCount == 0) { this.renderCard(); this.updateLayout(); } } private generateCardToRender(asPreview: boolean): Adaptive.AdaptiveCard { let cardToRender: Adaptive.AdaptiveCard = this.card; if (asPreview) { let inputPayload = this.card.toJSON(this._serializationContext); cardToRender = new Adaptive.AdaptiveCard(); cardToRender.hostConfig = this.card.hostConfig; let outputPayload = inputPayload; if (Shared.GlobalSettings.enableDataBindingSupport) { try { let template = new ACData.Template(inputPayload); let evaluationContext: ACData.IEvaluationContext; if (this.context.bindingPreviewMode === BindingPreviewMode.SampleData) { evaluationContext = { $root: this.context.sampleData, $host: this.context.sampleHostData }; } else { evaluationContext = { $root: this.context.dataStructure.dataType.generateSampleData(), $host: this.context.hostDataStructure.dataType.generateSampleData() }; } outputPayload = template.expand(evaluationContext); } catch (e) { console.log("Template expansion error: " + e.message); } } cardToRender.parse(outputPayload, this._serializationContext); } return cardToRender; } private renderCard() { this._cardHost.innerHTML = ""; if (this.onCardValidated) { let allValidationEvents: Adaptive.IValidationEvent[] = []; for (let i = 0; i < this._serializationContext.eventCount; i++) { allValidationEvents.push(this._serializationContext.getEventAt(i)); } allValidationEvents.push(...this.card.validateProperties().validationEvents); this.onCardValidated(allValidationEvents); } let cardToRender = this.generateCardToRender(this.isPreviewMode); if (this.isPreviewMode) { cardToRender.onExecuteAction = (action: Adaptive.Action) => { const actionType = action.getJsonTypeName(); let message = `Action executed: "${actionType}" title: "${action.title}"`; const verb = (action).verb; if (verb) { message += ` verb: "${verb}"`; } let carouselPageId: string | undefined = undefined; const root = action.getRootObject() as AdaptiveCard; if (root) { for (let i = 0; i < root.getItemCount(); i++) { let element = root.getItemAt(i); if (element instanceof Carousel) { carouselPageId = element.currentPageId; break; } } } if (carouselPageId) { message += `\ncarousel page id: "${carouselPageId}"`; } const url = (action).url; if (url) { message += `\nurl: "${url}"`; } const data = (action).data; if (data) { message += `\nSubmitted data: ${JSON.stringify(data, undefined, 4)}`; } alert(message); }; } let renderedCard = cardToRender.render(); if (this._cardHost.innerHTML === "") { this._cardHost.appendChild(renderedCard); } if (this.fixedHeightCard) { // truncate the content if the host container is fixed height if (this.isPreviewMode) { if (this.context.hostContainer?.requiresOverflowStyling()) { this.appendErrorMessage(Strings.widgetOverflowWarning); } else { renderedCard.style.overflow = "hidden"; } } this.context.hostContainer?.adjustStyleForBackground(); renderedCard.style.height = "100%"; } } private appendErrorMessage(message: string) { let errorElement = document.createElement("div"); errorElement.className = "acd-error-pane-message"; errorElement.innerText = message; errorElement.style.whiteSpace = "normal"; const errorPane = document.getElementById("errorPane"); errorPane.appendChild(errorElement); } private addPeer(peer: DesignerPeers.DesignerPeer, insertAfterNeighbor: boolean = false) { if (this._allPeers.indexOf(peer) < 0) { this._allPeers.push(peer); peer.render(); if (peer instanceof DesignerPeers.CardElementPeer) { peer.renderedElement.style.display = peer.isVisible() ? "initial" : "none"; // If we have a card element that we want to be selected after rendering, save the corresponding peer if (this._persistentSelectedCardElement && this._persistentSelectedCardElement === peer.cardElement) { this._persistentSelectedPeer = peer; this._persistentSelectedCardElement = null; } } peer.onSelectedChanged = (peer: DesignerPeers.DesignerPeer) => { if (peer.isSelected) { this.setSelectedPeer(peer); } else { if (this._selectedPeer == peer) { this.setSelectedPeer(null); } } }; peer.onChanged = (sender: DesignerPeers.DesignerPeer, updatePropertySheet: boolean) => { this.peerChanged(sender, updatePropertySheet); }; peer.onPeerRemoved = (sender: DesignerPeers.DesignerPeer) => { this.peerRemoved(sender); }; peer.onPeerAdded = (sender: DesignerPeers.DesignerPeer, newPeer: DesignerPeers.DesignerPeer) => { this.addPeer(newPeer, newPeer.insertAfterNeighbor); newPeer.insertAfterNeighbor = false; this.updateLayout(); }; peer.onStartDrag = (sender: DesignerPeers.DesignerPeer) => { this.startDrag(sender); } peer.onEndDrag = (sender: DesignerPeers.DesignerPeer) => { this.endDrag(false); } if (insertAfterNeighbor) { peer.addElementsToDesignerSurface(this._designerSurface, this.getPeerDOMNeighbor(peer)); } else { peer.addElementsToDesignerSurface(this._designerSurface); } for (let i = 0; i < peer.getChildCount(); i++) { this.addPeer(peer.getChildAt(i), insertAfterNeighbor); } } } private internalFindDropTarget(pointerPosition: IPoint, currentPeer: DesignerPeers.DesignerPeer, forPeer: DesignerPeers.DesignerPeer): DesignerPeers.DesignerPeer { if (currentPeer == forPeer) { return null; } var result: DesignerPeers.DesignerPeer = null; var lookDeeper = currentPeer instanceof DesignerPeers.ActionPeer; if (!lookDeeper && (currentPeer as DesignerPeers.CardElementPeer)?.isVisible()) { var boundingRect = currentPeer.getBoundingRect(); lookDeeper = boundingRect.isInside(pointerPosition); } if (lookDeeper) { let canDrop = currentPeer.canDrop(forPeer); if (canDrop) { result = currentPeer; } for (let i = 0; i < currentPeer.getChildCount(); i++) { var deeperResult = this.internalFindDropTarget(pointerPosition, currentPeer.getChildAt(i), forPeer); if (deeperResult) { result = deeperResult; break; } } } return result; } private findCardElementPeer(cardElement: Adaptive.CardElement): DesignerPeers.CardElementPeer { for (let i = 0; i < this._allPeers.length; i++) { var peer = this._allPeers[i]; if (peer instanceof DesignerPeers.CardElementPeer && peer.cardElement == cardElement) { return peer; } } return null; } private findActionPeer(action: Adaptive.Action): DesignerPeers.ActionPeer { for (let i = 0; i < this._allPeers.length; i++) { var peer = this._allPeers[i]; if (peer instanceof DesignerPeers.ActionPeer && peer.action == action) { return peer; } } return null; } private inlineCardExpanded(action: Adaptive.ShowCardAction, isExpanded: boolean) { let peer = this.findCardElementPeer(action.card); if (isExpanded) { if (!peer) { let registration = CardDesignerSurface.cardElementPeerRegistry.findTypeRegistration(Adaptive.AdaptiveCard); peer = new registration.peerType(peer, this, registration, action.card); peer.insertAfterNeighbor = true; let parentPeer = this.findActionPeer(action); if (parentPeer) { parentPeer.insertChild(peer); } else { this._rootPeer.insertChild(peer); } } else { peer.addElementsToDesignerSurface(this._designerSurface, this.getPeerDOMNeighbor(peer)); } } else { if (peer) { peer.removeElementsFromDesignerSurface(true); } } this.updateLayout(); } private get card(): Adaptive.AdaptiveCard { return this._card; } private setDraggedPeer(value: DesignerPeers.DesignerPeer) { if (this._draggedPeer != value) { if (this._draggedPeer) { this._draggedPeer.dragging = false; } this._draggedPeer = value; if (this._draggedPeer) { this._draggedPeer.dragging = true; } } } constructor(readonly context: DesignContext) { this._serializationContext = this.context.hostContainer.createSerializationContext(this.context.targetVersion); var rootElement = document.createElement("div"); rootElement.style.position = "relative"; rootElement.style.width = "100%"; rootElement.style.height = "100%"; this._cardHost = document.createElement("div"); this._cardHost.style.height = "100%"; rootElement.appendChild(this._cardHost); this._designerSurface = document.createElement("div"); this._designerSurface.classList.add("acd-designersurface"); this._designerSurface.tabIndex = -1; this._designerSurface.style.position = "absolute"; this._designerSurface.style.left = "0"; this._designerSurface.style.top = "0"; this._designerSurface.style.width = "100%"; this._designerSurface.style.height = "100%"; this._designerSurface.onkeyup = (e: KeyboardEvent) => { if (this._selectedPeer) { switch (e.key) { case Constants.keys.escape: if (this.draggedPeer) { this.endDrag(true); } else { this.setSelectedPeer(this._selectedPeer.parent); } break; case Constants.keys.backspace: case Constants.keys.delete: if (!this.draggedPeer) { this.removeSelected(); } break; case Constants.keys.enter: if (this._selectedPeer instanceof DesignerPeers.ActionPeer) { this._selectedPeer.action.renderedElement.click(); } break; } } return !e.cancelBubble; } this._designerSurface.onpointermove = (e: PointerEvent) => { let clientRect = this._designerSurface.getBoundingClientRect(); if (this.draggedPeer) { if (this._designerSurface.hasPointerCapture && !this._designerSurface.hasPointerCapture(e.pointerId)) { this._designerSurface.setPointerCapture(e.pointerId); } if (!this._dragVisual) { this._dragVisual = document.createElement("div"); this._dragVisual.style.pointerEvents = "none"; this._dragVisual.style.backgroundColor = "white"; this._dragVisual.style.padding = "6px"; this._dragVisual.style.opacity = "0.6"; this._dragVisual.style.boxShadow = "0 0 15px -5px rgba(0, 0, 0, 0.4)"; this._dragVisual.style.position = "absolute"; this._dragVisual.style.boxSizing = "content-box"; this._dragVisual.appendChild(this.draggedPeer.getCardObject().renderedElement.cloneNode(true)); document.body.appendChild(this._dragVisual); } const adjustedPoint = Utils.adjustPointForScroll(e); this._dragVisual.style.left = (adjustedPoint.x - 6) + "px"; this._dragVisual.style.top = (adjustedPoint.y - 6) + "px"; let renderedCardObjectRect = this.draggedPeer.getCardObject().renderedElement.getBoundingClientRect(); this._dragVisual.style.width = renderedCardObjectRect.width + "px"; this._dragVisual.style.height = renderedCardObjectRect.height + "px"; // When dragging on a new element, we set the zIndex higher so that it is always visible this._dragVisual.style.zIndex = "500"; this.tryDrop({ x: e.x - clientRect.left, y: e.y - clientRect.top }, this.draggedPeer); } } this._designerSurface.onpointerup = (e: PointerEvent) => { this._designerSurface.releasePointerCapture(e.pointerId); if (this.draggedPeer) { this.endDrag(false); } } rootElement.appendChild(this._designerSurface); this.context.hostContainer.cardHost.innerHTML = ""; this.context.hostContainer.cardHost.appendChild(rootElement); this._card = new Adaptive.AdaptiveCard(); this._card.onInlineCardExpanded = (action: Adaptive.ShowCardAction, isExpanded: boolean) => { this.inlineCardExpanded(action, isExpanded); }; this._card.onPreProcessPropertyValue = (sender: Adaptive.CardObject, property: Adaptive.PropertyDefinition, value: any) => { if (Shared.GlobalSettings.enableDataBindingSupport && typeof value === "string" && this.context.sampleData && this.context.bindingPreviewMode !== BindingPreviewMode.NoPreview) { let expression = ACData.Template.parseInterpolatedString(value); if (typeof expression === "string") { return expression; } else { let evaluationContext: ACData.IEvaluationContext; if (this.context.bindingPreviewMode === BindingPreviewMode.SampleData) { evaluationContext = { $root: this.context.sampleData, $host: this.context.sampleHostData }; } else { evaluationContext = { $root: this.context.dataStructure.dataType.generateSampleData(), $host: this.context.hostDataStructure.dataType.generateSampleData() }; } let evaluationResult = ACData.Template.tryEvaluateExpression(expression, evaluationContext, true); return typeof evaluationResult.value === "string" ? evaluationResult.value : value; } } return value; }; this._card.version = this.context.targetVersion; this._card.hostConfig = this.context.hostContainer.getHostConfig(); this._card.designMode = true; Adaptive.AdaptiveCard.onCarouselEvent = (e: Adaptive.CarouselEvent) => { if (!this._isPreviewMode) { this.render(); } } this.render(); } onCardValidated: (logEntries: Adaptive.IValidationEvent[]) => void; onSelectedPeerChanged: (peer: DesignerPeers.DesignerPeer) => void; onLayoutUpdated: (isFullRefresh: boolean) => void; fixedHeightCard: boolean = false; getDesignerSurfaceOffset(): IPoint { let clientRect = this._designerSurface.getBoundingClientRect(); return { x: clientRect.left, y: clientRect.top }; } findDropTarget(pointerPosition: IPoint, peer: DesignerPeers.DesignerPeer): DesignerPeers.DesignerPeer { return this.internalFindDropTarget(pointerPosition, this._rootPeer, peer); } findPeer(cardObject: Adaptive.CardObject) { for (let peer of this._allPeers) { if (peer.getCardObject() === cardObject) { return peer; } } return undefined; } beginUpdate() { this._updateCount++; } endUpdate(renderCard: boolean) { if (this._updateCount > 0) { this._updateCount--; if (this._updateCount == 0) { if (renderCard) { this.renderCard(); } this.updateLayout(); } } } render() { this._designerSurface.innerHTML = ""; this._allPeers = []; // If we want to have the same peer selected after rendering the card, // store the current selected peer's card element before the peers recreated if (this._shouldPersistSelectedElement && this._selectedPeer && this._selectedPeer instanceof DesignerPeers.CardElementPeer) { this._persistentSelectedCardElement = this._selectedPeer.cardElement; this._shouldPersistSelectedElement = false; } this.setSelectedPeer(null); this.renderCard(); this._rootPeer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance(this, null, this.card); this.addPeer(this._rootPeer); this._removeCommandElement = document.createElement("div"); this._removeCommandElement.classList.add("acd-peerButton", "acd-peerButton-icon", "fixedWidth", "circular", "acd-icon-remove"); this._removeCommandElement.title = "Remove"; this._removeCommandElement.setAttribute("aria-label", "Remove"); this._removeCommandElement.setAttribute("role", "button"); this._removeCommandElement.style.visibility = "hidden"; this._removeCommandElement.style.position = "absolute"; this._removeCommandElement.style.zIndex = "500"; this._removeCommandElement.tabIndex = 0; this._removeCommandElement.onclick = (e) => { this.removeSelected(); } this._removeCommandElement.onkeyup = (e: KeyboardEvent) => { if (e.key === Constants.keys.enter) { this.removeSelected(); } }; this._dragHandle = new DragHandle(); this._dragHandle.onStartDrag = (sender) => { this._dragHandle.endDrag(); this.startDrag(this._selectedPeer); } this._dragHandle.render(); this._peerCommandsHostElement = document.createElement("div"); this._peerCommandsHostElement.style.visibility = "hidden"; this._peerCommandsHostElement.style.position = "absolute"; this._peerCommandsHostElement.style.display = "flex"; this._peerCommandsHostElement.style.justifyContent = "flex-end"; this._peerCommandsHostElement.style.zIndex = "500"; this._peerCommandsHostElement.style.pointerEvents = "none"; this.updateLayout(); // If we have a persistent selected peer, select the peer // We should only reach this point is _shouldPersistSelectedElement = true prior to rendering the card if (this._persistentSelectedPeer) { this.setSelectedPeer(this._persistentSelectedPeer); this._persistentSelectedPeer = null; } } getCardPayloadAsObject(): object { return this.card.toJSON(this._serializationContext); } setCardPayloadAsObject(payload: object) { this._serializationContext.clearEvents(); this.card.parse(payload, this._serializationContext); this.render(); } setCardPayloadAsString(payload: string) { try { this.setCardPayloadAsObject(JSON.parse(payload)); } catch (e) { console.warn("Invalid JSON string. " + e); } } getBoundCardPayloadAsObject(): object { return this.generateCardToRender(true).toJSON(this._serializationContext); } updateLayout(isFullRefresh: boolean = true) { if (!this.isPreviewMode) { for (let i = 0; i < this._allPeers.length; i++) { this._allPeers[i].updateLayout(); } this.updatePeerCommandsLayout(); if (this.onLayoutUpdated) { this.onLayoutUpdated(isFullRefresh); } } } removeSelected() { if (this.selectedPeer) { this.beginUpdate(); try { let parent = this.selectedPeer.parent; if (this.selectedPeer.remove(false, true)) { this.setSelectedPeer(parent); parent.focus(); } } finally { this.endUpdate(true); // If we've removed a Carousel page, we need render the designer surface again if (this.selectedPeer instanceof DesignerPeers.CarouselPeer) { (this.selectedPeer.cardElement as Carousel).currentIndex = 0; this.render(); } } } } onStartDrag: (sender: CardDesignerSurface) => void; onEndDrag: (sender: CardDesignerSurface, wasCancelled: boolean) => void; startDrag(peer: DesignerPeers.DesignerPeer) { if (!this.draggedPeer) { this._designerSurface.classList.add("dragging"); this.setDraggedPeer(peer); this.setSelectedPeer(this.draggedPeer); if (this.onStartDrag) { this.onStartDrag(this); } } } endDrag(wasCancelled: boolean) { if (this.draggedPeer) { // Ensure that the dragged peer's elements are at the top in Z order this.draggedPeer.removeElementsFromDesignerSurface(true); this.draggedPeer.addElementsToDesignerSurface(this._designerSurface, this.getPeerDOMNeighbor(this._draggedPeer)); this._dropTarget.renderedElement.classList.remove("dragover"); this._dragVisual?.parentNode.removeChild(this._dragVisual); this._dragVisual = undefined; this.setDraggedPeer(null); if (this.onEndDrag) { this.onEndDrag(this, wasCancelled); } this._designerSurface.classList.remove("dragging"); } } // Find the element directly above the dragged element getPeerDOMNeighbor(peer: DesignerPeers.DesignerPeer): HTMLElement { if (peer.parent) { let neighboringPeer = peer.parent; if (neighboringPeer instanceof DesignerPeers.ActionSetPeer) { // A new action is being added to an ActionSet, so we can add the ActionPeer as the last child element const childCount = neighboringPeer.getChildCount(); if (childCount > 1) { // Subtract 2 because the child count already includes the action being currently added neighboringPeer = neighboringPeer.getChildAt(childCount - 2); } } else if (neighboringPeer instanceof DesignerPeers.ActionPeer) { // neighboringPeer should be the parent so we can get the last element in the actionSet neighboringPeer = neighboringPeer.parent; neighboringPeer = neighboringPeer.getChildAt(neighboringPeer.getChildCount() - 1); } else if (peer instanceof DesignerPeers.CardElementPeer) { // Get the index of the peer within its container const peerIndex = peer.cardElement.index; // If it is not the first element, find the neighbor if (peerIndex > 0) { neighboringPeer = neighboringPeer.getChildAt(peerIndex - 1); // If the neighbor has children (i.e. has elements below it in the html tree), find its last child if (neighboringPeer.getChildCount() > 0) { neighboringPeer = this.getLastPeerInContainer(neighboringPeer); } } } return neighboringPeer.renderedElement; } // Return undefined if there is no parent return undefined; } // Recursive method to find a containers last element getLastPeerInContainer(parentContainer: DesignerPeers.DesignerPeer): DesignerPeers.DesignerPeer { let lastChildPeer = parentContainer.getChildAt(parentContainer.getChildCount() - 1); if (lastChildPeer.getChildCount() > 0) { lastChildPeer = this.getLastPeerInContainer(lastChildPeer); } return lastChildPeer; } tryDrop(pointerPosition: IPoint, peer: DesignerPeers.DesignerPeer): boolean { if (peer) { if (this._dropTarget) { this._dropTarget.renderedElement.classList.remove("dragover"); } var newDropTarget = this.findDropTarget(pointerPosition, peer); if (newDropTarget) { this._dropTarget = newDropTarget; this._dropTarget.renderedElement.classList.add("dragover"); return this._dropTarget.tryDrop(peer, pointerPosition); } } return false; } isPointerOver(x: number, y: number) { let clientRect = this._designerSurface.getBoundingClientRect(); return (x >= clientRect.left) && (x <= clientRect.right) && (y >= clientRect.top) && (y <= clientRect.bottom); } pageToClientCoordinates(x: number, y: number): IPoint { let clientRect = this._designerSurface.getBoundingClientRect(); return { x: x - clientRect.left, y: y - clientRect.top } } get rootPeer(): DesignerPeers.DesignerPeer { return this._rootPeer; } get selectedPeer(): DesignerPeers.DesignerPeer { return this._selectedPeer; } get draggedPeer(): DesignerPeers.DesignerPeer { return this._draggedPeer; } get isPreviewMode(): boolean { return this._isPreviewMode; } set isPreviewMode(value: boolean) { if (this._isPreviewMode != value) { this._isPreviewMode = value; if (this._isPreviewMode) { this._designerSurface.classList.add("acd-hidden"); this._dragHandle.renderedElement.classList.add("acd-hidden"); this._removeCommandElement.classList.add("acd-hidden"); this._peerCommandsHostElement.classList.add("acd-hidden"); } else { this._designerSurface.classList.remove("acd-hidden"); this._dragHandle.renderedElement.classList.remove("acd-hidden"); this._removeCommandElement.classList.remove("acd-hidden"); this._peerCommandsHostElement.classList.remove("acd-hidden"); } this.card.designMode = !this._isPreviewMode; this.renderCard(); this.updateLayout(false); } } set shouldPersistSelectedElement(shouldPersistSelectedElement: boolean) { this._shouldPersistSelectedElement = shouldPersistSelectedElement; } }