// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as Adaptive from "adaptivecards"; import * as Controls from "adaptivecards-controls"; import { DraggableElement } from "./draggable-element"; import { PeerCommand } from "./peer-command"; import { CardDesignerSurface, DesignContext } from "./card-designer-surface"; import { DesignerPeerTreeItem } from "./designer-peer-treeitem"; import { Rect, IPoint, Utils } from "./miscellaneous"; import { GlobalSettings } from "./shared"; import { FieldPicker } from "./field-picker"; import { Strings } from "./strings"; export abstract class DesignerPeerInplaceEditor { onClose: (applyChanges: boolean) => void; abstract initialize(); abstract applyChanges(); abstract render(): HTMLElement; } export abstract class CardElementPeerInplaceEditor extends DesignerPeerInplaceEditor { readonly cardElement: TCardElement; constructor(cardElement: TCardElement) { super(); this.cardElement = cardElement; } } export class DesignerPeerRegistrationBase { private _iconClass?: string; readonly category: string; constructor(category: string, iconClass?: string) { this.category = category; this._iconClass = iconClass; } get iconClass(): string { return this._iconClass ? this._iconClass : "acd-icon-defaultElementIcon"; } } export class DesignerPeerRegistration extends DesignerPeerRegistrationBase{ readonly sourceType: TSource; peerType: TPeer; constructor(sourceType: TSource, peerType: TPeer, category: string, iconClass: string = null) { super(category, iconClass); this.sourceType = sourceType; this.peerType = peerType; } } export class PropertySheetCategory { static readonly DefaultCategory = "__defaultCategory"; static readonly LayoutCategory = "Layout"; static readonly StyleCategory = "Style"; static readonly SelectionAction = "Selection action"; static readonly InlineAction = "Inline action"; static readonly Validation = "Validation"; static readonly Refresh = "Refresh"; private _entries: PropertySheetEntry[] = []; constructor(readonly name: string) { } render(container: Adaptive.Container, context: PropertySheetContext, displayCategoryName: boolean) { let entriesToRender: PropertySheetEntry[] = []; for (let entry of this._entries) { if (Adaptive.isVersionLessOrEqual(entry.targetVersion, context.designContext.targetVersion)) { entriesToRender.push(entry); } } if (entriesToRender.length > 0) { if (displayCategoryName) { let header = new Adaptive.TextBlock(); header.separator = true; header.text = "**" + (this.name === PropertySheetCategory.DefaultCategory ? context.peer.getCardObject().getJsonTypeName() : this.name) + "**"; container.addItem(header); } for (let entry of entriesToRender) { if (Adaptive.isVersionLessOrEqual(entry.targetVersion, context.designContext.targetVersion)) { container.addItem(entry.render(context)); } } } } add(...entries: PropertySheetEntry[]) { this._entries = this._entries.concat(entries); } remove(...entries: PropertySheetEntry[]) { for (let entry of entries) { let index: number; do { let index = this._entries.indexOf(entry); if (index >= 0) { this._entries.splice(index, 1); } } while (index >= 0); } } getEntryAt(index: number): PropertySheetEntry { return this._entries[index]; } get length(): number { return this._entries.length; } } export class PropertySheet { private _categories: { [key: string]: PropertySheetCategory } = {}; constructor(readonly displayCategoryNames: boolean = true) { this._categories[PropertySheetCategory.DefaultCategory] = new PropertySheetCategory(PropertySheetCategory.DefaultCategory); this._categories[PropertySheetCategory.LayoutCategory] = new PropertySheetCategory(PropertySheetCategory.LayoutCategory); this._categories[PropertySheetCategory.StyleCategory] = new PropertySheetCategory(PropertySheetCategory.StyleCategory); } add(categoryName: string, ...entries: PropertySheetEntry[]) { let category = this._categories[categoryName]; if (!category) { category = new PropertySheetCategory(categoryName); this._categories[categoryName] = category; } category.add(...entries); } addActionProperties( targetVersion: Adaptive.Version, peer: DesignerPeer, action: Adaptive.Action, category: string, excludeProperties: PropertySheetEntry[] = [ ActionPeer.iconUrlProperty, ActionPeer.styleProperty, ActionPeer.modeProperty ]) { let actionPeer = CardDesignerSurface.actionPeerRegistry.createPeerInstance(peer.designerSurface, null, action); actionPeer.onChanged = (sender: DesignerPeer, updatePropertySheet: boolean) => { peer.changed(updatePropertySheet); }; let subPropertySheet = new PropertySheet(false); actionPeer.populatePropertySheet(subPropertySheet, category); if (excludeProperties.length > 0) { subPropertySheet.remove(...excludeProperties); } this.add( category, new SubPropertySheetEntry(targetVersion, action, subPropertySheet)); } remove(...entries: PropertySheetEntry[]) { for (let categoryName of Object.keys(this._categories)) { this._categories[categoryName].remove(...entries); } } render(container: Adaptive.Container, context: PropertySheetContext) { for (let categoryName of Object.keys(this._categories)) { this._categories[categoryName].render(container, context, this.displayCategoryNames); } } } export class PropertySheetContext { private _target: object = undefined; constructor( readonly designContext: DesignContext, readonly peer: DesignerPeer, target: object = undefined) { this._target = target; } get target(): object { return this._target != undefined ? this._target : this.peer.getCardObject(); } } export abstract class PropertySheetEntry { abstract render(context: PropertySheetContext): Adaptive.CardElement; constructor(readonly targetVersion: Adaptive.TargetVersion) { } } export class SubPropertySheetEntry { render(context: PropertySheetContext): Adaptive.CardElement { let container = new Adaptive.Container(); container.spacing = Adaptive.Spacing.Small; this.propertySheet.render(container, new PropertySheetContext( context.designContext, context.peer, this.target)); return container; } constructor(readonly targetVersion: Adaptive.TargetVersion, readonly target: object, readonly propertySheet: PropertySheet) { } } export class CustomPropertySheetEntry extends PropertySheetEntry { render(context: PropertySheetContext): Adaptive.CardElement { if (this.onRender) { return this.onRender(context); } } constructor(readonly targetVersion: Adaptive.TargetVersion, readonly onRender: (context: PropertySheetContext) => Adaptive.CardElement) { super(targetVersion); } } export interface IPropertySheetEditorCommand { id?: string; caption: string; altText?: string; expanded?: boolean; onExecute: (sender: SingleInputPropertyEditor, clickedElement: HTMLElement) => void; } export abstract class SingleInputPropertyEditor extends PropertySheetEntry { protected abstract createInput(context: PropertySheetContext): Adaptive.Input; protected getPropertyValue(context: PropertySheetContext): any { return context.target[this.propertyName]; } protected setPropertyValue(context: PropertySheetContext, value: string) { context.target[this.propertyName] = value; } protected getAdditionalCommands(context: PropertySheetContext): IPropertySheetEditorCommand[] { return []; } render(context: PropertySheetContext): Adaptive.CardElement { let leftColumn = new Adaptive.Column(); leftColumn.width = new Adaptive.SizeAndUnit(100, Adaptive.SizeUnit.Pixel); leftColumn.verticalContentAlignment = Adaptive.VerticalAlignment.Center; let rightColumn = new Adaptive.Column(); rightColumn.width = "stretch"; rightColumn.verticalContentAlignment = Adaptive.VerticalAlignment.Center; let columnSet = new Adaptive.ColumnSet(); columnSet.spacing = Adaptive.Spacing.Small; columnSet.addColumn(leftColumn); columnSet.addColumn(rightColumn); let label = new Adaptive.TextBlock(); label.horizontalAlignment = Adaptive.HorizontalAlignment.Right; label.wrap = true; label.text = this.label; label.id = Adaptive.generateUniqueId(); let input = this.createInput(context); input.labelledBy = label.id; input.onValueChanged = () => { this.setPropertyValue(context, input.value); context.peer.changed(this.causesPropertySheetRefresh); } leftColumn.addItem(label); rightColumn.addItem(input); let additionalCommands = this.getAdditionalCommands(context); if (additionalCommands && additionalCommands.length > 0) { let commandColumn = new Adaptive.Column(); commandColumn.width = "auto"; commandColumn.spacing = Adaptive.Spacing.Small; commandColumn.verticalContentAlignment = Adaptive.VerticalAlignment.Center; let actionSet = new Adaptive.ActionSet(); for (let command of additionalCommands) { let action = new Adaptive.SubmitAction(); action.id = command.id; action.title = command.caption; action.tooltip = command.altText; if (command.expanded) { action.state = Adaptive.ActionButtonState.Expanded; } action.onExecute = (sender: Adaptive.Action) => { command.onExecute(this, sender.renderedElement); }; actionSet.addAction(action); } commandColumn.addItem(actionSet); columnSet.addColumn(commandColumn); } return columnSet; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion); } } export class BaseStringPropertyEditor extends SingleInputPropertyEditor { protected placeHolder: string = "(not set)"; protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.TextInput(); input.defaultValue = this.getPropertyValue(context); input.placeholder = this.placeHolder; input.isMultiline = this.isMultiline; return input; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly allowBinding: boolean = false, readonly isMultiline: boolean = false, readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion, propertyName, label, causesPropertySheetRefresh); } } export class StringPropertyEditor extends BaseStringPropertyEditor { protected getAdditionalCommands(context: PropertySheetContext): IPropertySheetEditorCommand[] { if (GlobalSettings.enableDataBindingSupport && this.allowBinding) { return [ { id: this.propertyName + "BindData", caption: Strings.toolboxes.propertySheet.commands.bindData.displayText(), altText: Strings.toolboxes.propertySheet.commands.bindData.accessibleText(this.label), expanded: false, onExecute: (sender: SingleInputPropertyEditor, clickedElement: HTMLElement) => { clickedElement.setAttribute("aria-expanded", "true"); // TODO: Add host parameter to field picker let fieldPicker = new FieldPicker(context.designContext.dataStructure); fieldPicker.onClose = (sender, wasCancelled) => { clickedElement.setAttribute("aria-expanded", "false"); if (!wasCancelled) { this.setPropertyValue(context, fieldPicker.selectedField.asExpression()); clickedElement.focus(); context.peer.changed(true); } clickedElement.focus(); } fieldPicker.popup(clickedElement); } } ]; } else { return super.getAdditionalCommands(context); } } } export class StringArrayPropertyEditor extends BaseStringPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let rawValue = context.target[this.propertyName]; let result: string[] = []; if (rawValue !== undefined && Array.isArray(rawValue)) { for (let item of rawValue) { if (typeof item === "string") { result.push(item); } } } return result.length > 0 ? result.join(",") : undefined; } protected setPropertyValue(context: PropertySheetContext, value: string) { if (value !== undefined) { let rawValues = value.split(","); let result: string[] = []; for (let rawValue of rawValues) { if (rawValue) { result.push(rawValue); } } context.target[this.propertyName] = result.length > 0 ? result : undefined; } } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly allowBinding: boolean = false, readonly isMultiline: boolean = false, readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion, propertyName, label, allowBinding, isMultiline, causesPropertySheetRefresh); this.placeHolder = "(not set, comma-separated list)"; } } export class NumberPropertyEditor extends SingleInputPropertyEditor { protected _input: Adaptive.NumberInput; protected setPropertyValue(context: PropertySheetContext, value: string) { try { context.target[this.propertyName] = parseFloat(value); } catch { context.target[this.propertyName] = this.defaultValue; } } protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.NumberInput(); input.defaultValue = this.getPropertyValue(context) ?? this.defaultValue; input.placeholder = "(not set)"; this._input = input; return this._input; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly defaultValue: number | undefined = undefined, readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion, propertyName, label, causesPropertySheetRefresh); } } export class ObjectPropertyEditor extends StringPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { return JSON.stringify(context.target[this.propertyName]); } protected setPropertyValue(context: PropertySheetContext, value: string) { context.target[this.propertyName] = JSON.parse(value); } } export class CustomCardObjectPropertyEditor extends StringPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { return context.peer.getCardObject().getCustomProperty(this.propertyName); } protected setPropertyValue(context: PropertySheetContext, value: string) { context.peer.getCardObject().setCustomProperty(this.propertyName, value); } } export class BooleanPropertyEditor extends SingleInputPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let v = context.target[this.propertyName]; return typeof v === "boolean" ? v.toString() : "false"; } protected setPropertyValue(context: PropertySheetContext, value: string) { context.target[this.propertyName] = value == "true"; } protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.ToggleInput(); input.defaultValue = this.getPropertyValue(context); return input; } } export interface IVersionedChoice { targetVersion: Adaptive.TargetVersion; name: string; value: string; } const NotSetValue = "@@not_set@@"; export class ChoicePropertyEditor extends SingleInputPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let currentValue = context.target[this.propertyName]; return currentValue !== undefined ? currentValue.toString() : NotSetValue; } protected setPropertyValue(context: PropertySheetContext, value: string) { if (value === NotSetValue) { context.target[this.propertyName] = undefined; } else { context.target[this.propertyName] = value; } } protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.ChoiceSetInput(); input.defaultValue = this.getPropertyValue(context); input.isCompact = true; if (this.isNullable) { input.choices.push(new Adaptive.Choice("(not set)", NotSetValue)); } else { input.placeholder = "(not set)"; } for (let choice of this.choices) { if (Adaptive.isVersionLessOrEqual(choice.targetVersion, context.designContext.targetVersion)) { input.choices.push(new Adaptive.Choice(choice.name, choice.value)); } } return input; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly choices: IVersionedChoice[], readonly causesPropertySheetRefresh: boolean = false, readonly isNullable: boolean = false) { super(targetVersion, propertyName, label, causesPropertySheetRefresh); } } export class ContainerStylePropertyEditor extends ChoicePropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let currentStyle = context.target[this.propertyName]; return currentStyle ? currentStyle.toString() : NotSetValue; } protected setPropertyValue(context: PropertySheetContext, value: string) { if (value == NotSetValue) { context.target[this.propertyName] = undefined; } else { context.target[this.propertyName] = value; } } constructor(readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string) { super( targetVersion, propertyName, label, [ { targetVersion: Adaptive.Versions.v1_0, name: "(not set)", value: NotSetValue }, { targetVersion: Adaptive.Versions.v1_0, name: "Default", value: "default" }, { targetVersion: Adaptive.Versions.v1_0, name: "Emphasis", value: "emphasis" }, { targetVersion: Adaptive.Versions.v1_2, name: "Accent", value: "accent" }, { targetVersion: Adaptive.Versions.v1_2, name: "Good", value: "good" }, { targetVersion: Adaptive.Versions.v1_2, name: "Attention", value: "attention" }, { targetVersion: Adaptive.Versions.v1_2, name: "Warning", value: "warning" } ]); } } export class NullableBooleanPropertyEditor extends ChoicePropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let currentValue = context.target[this.propertyName]; return typeof currentValue === "boolean" ? currentValue.toString() : NotSetValue; } protected setPropertyValue(context: PropertySheetContext, value: string) { if (value === NotSetValue) { context.target[this.propertyName] = undefined; } else { context.target[this.propertyName] = value === "true"; } } constructor(readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string) { super( targetVersion, propertyName, label, [ { targetVersion: Adaptive.Versions.v1_0, name: "(not set)", value: NotSetValue }, { targetVersion: Adaptive.Versions.v1_0, name: "True", value: "true" }, { targetVersion: Adaptive.Versions.v1_0, name: "False", value: "false" } ]); } } export class ColumnWidthPropertyEditor extends ChoicePropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { if (context.target[this.propertyName] instanceof Adaptive.SizeAndUnit) { if (context.target[this.propertyName].unit == Adaptive.SizeUnit.Pixel) { return "pixels"; } else { return "weighted"; } } else { return context.target[this.propertyName].toString(); } } protected setPropertyValue(context: PropertySheetContext, value: string) { switch (value) { case "auto": context.target[this.propertyName] = "auto"; break; case "pixels": context.target[this.propertyName] = new Adaptive.SizeAndUnit(50, Adaptive.SizeUnit.Pixel); break; case "weighted": context.target[this.propertyName] = new Adaptive.SizeAndUnit(50, Adaptive.SizeUnit.Weight); break; case "stretch": default: context.target[this.propertyName] = "stretch"; break; } } } export class HeightPropertyEditor extends ChoicePropertyEditor { protected setPropertyValue(context: PropertySheetContext, value: string) { let processedValue: string; switch (value) { case "auto": case "stretch": processedValue = value; break; default: processedValue = "auto"; break; } context.target[this.propertyName] = processedValue; } } export class SizeAndUnitPropertyEditor extends NumberPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { return (context.target[this.propertyName]).physicalSize.toString(); } protected setPropertyValue(context: PropertySheetContext, value: string) { context.target[this.propertyName] = new Adaptive.SizeAndUnit(parseInt(value), this.sizeUnit); } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly sizeUnit: Adaptive.SizeUnit, readonly defaultValue: number | undefined = undefined, readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion, propertyName, label, defaultValue, causesPropertySheetRefresh); } } export class CarouselTimerPropertyEditor extends NumberPropertyEditor { protected setPropertyValue(context: PropertySheetContext, value: string) { try { const parsedValue = parseFloat(value); const minAutoplayDelay = context.designContext.hostContainer.getHostConfig().carousel.minAutoplayDelay; if (parsedValue < minAutoplayDelay) { // TODO: This causes a strange bug - basically cannot clear NumberInput and type a new number // Need to update where we call setPropertyValue/at least display a visual warning this._input.value = minAutoplayDelay; console.warn(Adaptive.Strings.errors.tooLittleTimeDelay); context.target[this.propertyName] = minAutoplayDelay; } else { context.target[this.propertyName] = parsedValue; } } catch { context.target[this.propertyName] = this.defaultValue; } } } export class ActionPropertyEditor extends SingleInputPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let action = context.target[this.propertyName]; return action ? action.getJsonTypeName() : "none"; } protected setPropertyValue(context: PropertySheetContext, value: string) { if (value == "none") { context.target[this.propertyName] = undefined; } else { context.target[this.propertyName] = context.designContext.hostContainer.actionsRegistry.createInstance(value, context.designContext.targetVersion); } } protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.ChoiceSetInput(); input.defaultValue = this.getPropertyValue(context); input.isCompact = true; input.placeholder = "(not set)"; input.choices.push(new Adaptive.Choice("(not set)", "none")); for (let i = 0; i < context.designContext.hostContainer.actionsRegistry.getItemCount(); i++) { let actionType = context.designContext.hostContainer.actionsRegistry.getItemAt(i).typeName; let excludeAction = true; if (this.forbiddenActionTypes) { // If the list contains "*", all action types are disallowed... excludeAction = this.forbiddenActionTypes.indexOf("*") >= 0 || this.forbiddenActionTypes.indexOf(actionType) >= 0; if (excludeAction) { // ...except if the list explicitly contains the type name prefixed with a "-" character // Example: // [ "*", "-Action.Execute", "-Action.Submit" ] // Meaning: all actions are disallowed except Action.Execute and Action.Submit excludeAction = this.forbiddenActionTypes.indexOf("-" + actionType) < 0; } } if (!excludeAction) { let choice = new Adaptive.Choice(actionType, actionType); input.choices.push(choice); } } return input; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly forbiddenActionTypes: string[] = [], readonly causesPropertySheetRefresh: boolean = false) { super(targetVersion, propertyName, label, causesPropertySheetRefresh); } } export class CompoundPropertyEditor extends PropertySheetEntry { render(context: PropertySheetContext): Adaptive.CardElement { let container = new Adaptive.Container(); let target = context.target[this.propertyName]; if (target === undefined && this.initializeProperty) { target = this.initializeProperty(); context.target[this.propertyName] = target; } if (target) { for (let entry of this.entries) { if (Adaptive.isVersionLessOrEqual(entry.targetVersion, context.designContext.targetVersion)) { container.addItem( entry.render( new PropertySheetContext( context.designContext, context.peer, target))); } } } return container; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly entries: PropertySheetEntry[] = [], readonly initializeProperty?: () => object) { super(targetVersion); } } export class EnumPropertyEditor extends SingleInputPropertyEditor { protected getPropertyValue(context: PropertySheetContext): any { let value = context.target[this.propertyName]; return value !== undefined ? value : "-1"; } protected setPropertyValue(context: PropertySheetContext, value: string) { let valueAsInt = parseInt(value, 10); if (valueAsInt >= 0) { context.target[this.propertyName] = valueAsInt; } else { context.target[this.propertyName] = undefined; } } protected createInput(context: PropertySheetContext): Adaptive.Input { let input = new Adaptive.ChoiceSetInput(); input.isCompact = true; let defaultValue = this.getPropertyValue(context); input.defaultValue = defaultValue !== undefined ? defaultValue : "-1"; if (this.isNullable) { input.choices.push(new Adaptive.Choice("(not set)", "-1")); } else { input.placeholder = "(not set)"; } for (let key in this.enumType) { let v = parseInt(key, 10); if (!isNaN(v)) { input.choices.push(new Adaptive.Choice(this.enumType[key], key)); } } return input; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly propertyName: string, readonly label: string, readonly enumType: { [s: number]: string }, readonly causesPropertySheetRefresh: boolean = false, readonly isNullable: boolean = false) { super(targetVersion, propertyName, label, causesPropertySheetRefresh); } } interface INameValuePair { name: string; value: string; } class NameValuePairPropertyEditor extends PropertySheetEntry { private collectionChanged(context: PropertySheetContext, nameValuePairs: INameValuePair[], refreshPropertySheet: boolean) { context.target[this.collectionPropertyName] = []; for (let nameValuePair of nameValuePairs) { let item = this.createCollectionItem(nameValuePair.name, nameValuePair.value); context.target[this.collectionPropertyName].push(item); } context.peer.changed(refreshPropertySheet); } render(context: PropertySheetContext): Adaptive.CardElement { let result = new Adaptive.Container(); let collection = context.target[this.collectionPropertyName]; if (!Array.isArray(collection)) { throw new Error("The " + this.collectionPropertyName + " property on " + context.peer.getCardObject().getJsonTypeName() + " either doesn't exist or isn't an array.") } let nameValuePairs: INameValuePair[] = []; for (let pair of collection) { nameValuePairs.push( { name: pair[this.namePropertyName], value: pair[this.valuePropertyName] } ); } if (nameValuePairs.length == 0) { let messageTextBlock = new Adaptive.TextBlock(); messageTextBlock.spacing = Adaptive.Spacing.Small; messageTextBlock.text = this.messageIfEmpty; result.addItem(messageTextBlock); } else { for (let i = 0; i < nameValuePairs.length; i++) { let textInput = new Adaptive.TextInput(); textInput.placeholder = this.namePropertyLabel; textInput.defaultValue = nameValuePairs[i].name; textInput.onValueChanged = (sender) => { nameValuePairs[i].name = sender.value; this.collectionChanged(context, nameValuePairs, false); }; let nameColumn = new Adaptive.Column("stretch"); nameColumn.addItem(textInput); textInput = new Adaptive.TextInput(); textInput.placeholder = this.valuePropertyLabel; textInput.defaultValue = nameValuePairs[i].value; textInput.onValueChanged = (sender) => { nameValuePairs[i].value = sender.value; this.collectionChanged(context, nameValuePairs, false); }; let valueColumn = new Adaptive.Column("stretch"); valueColumn.spacing = Adaptive.Spacing.Small; valueColumn.addItem(textInput); let removeAction = new Adaptive.SubmitAction(); removeAction.iconUrl = require("./assets/xmark.svg"); removeAction.tooltip = "Remove"; removeAction.onExecute = (sender) => { nameValuePairs.splice(i, 1); this.collectionChanged(context, nameValuePairs, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.addAction(removeAction); let removeColumn = new Adaptive.Column("auto"); removeColumn.spacing = Adaptive.Spacing.Small; removeColumn.addItem(actionSet); let columnSet = new Adaptive.ColumnSet(); columnSet.spacing = Adaptive.Spacing.Small; columnSet.addColumn(nameColumn); columnSet.addColumn(valueColumn); columnSet.addColumn(removeColumn); result.addItem(columnSet); } } let addAction = new Adaptive.SubmitAction(); addAction.title = this.addButtonTitle; addAction.onExecute = (sender) => { nameValuePairs.push({ name: "", value: "" }); this.collectionChanged(context, nameValuePairs, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.spacing = Adaptive.Spacing.Small; actionSet.addAction(addAction); result.addItem(actionSet); return result; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly collectionPropertyName: string, readonly namePropertyName: string, readonly valuePropertyName: string, readonly createCollectionItem: (name: string, value: string) => any, readonly namePropertyLabel: string = "Name", readonly valuePropertyLabel: string = "Value", readonly addButtonTitle: string = "Add", readonly messageIfEmpty: string = "This collection is empty") { super(targetVersion); } } type NameAndValue = { name: string; value?: string; }; type InnerPropertiesDictionary = { [name: string]: NameAndValue; }; class InnerStructPropertyEditor extends PropertySheetEntry { private collectionChanged(context: PropertySheetContext, innerPropertiesList: InnerPropertiesDictionary[], refreshPropertySheet: boolean) { context.target[this.collectionPropertyName] = []; const collectionItems = innerPropertiesList.map(e => this.createCollectionItem(e)); context.target[this.collectionPropertyName].push(...collectionItems); context.peer.changed(refreshPropertySheet); } render(context: PropertySheetContext): Adaptive.CardElement { const result = new Adaptive.Container(); let collection = context.target[this.collectionPropertyName]; if (!Array.isArray(collection)) { throw new Error("The " + this.collectionPropertyName + " property on " + context.peer.getCardObject().getJsonTypeName() + " either doesn't exist or isn't an array.") } let innerPropertiesList: InnerPropertiesDictionary[] = []; for (let innerProperties of collection) { var newItem : InnerPropertiesDictionary = {}; Object.keys(this.innerPropertiesDefaults).forEach(key => newItem[key] = { name: this.innerPropertiesDefaults[key].name, value: innerProperties[key]}); innerPropertiesList.push(newItem); } if (innerPropertiesList.length == 0) { let messageTextBlock = new Adaptive.TextBlock(); messageTextBlock.spacing = Adaptive.Spacing.Small; messageTextBlock.text = this.messageIfEmpty; result.addItem(messageTextBlock); } else { for (let i = 0; i < innerPropertiesList.length; i++) { let columnSet = new Adaptive.ColumnSet(); columnSet.spacing = Adaptive.Spacing.Medium; columnSet.separator = true; let newColumn = new Adaptive.Column("stretch"); Object.keys(innerPropertiesList[i]).forEach(key => { let textInput = new Adaptive.TextInput(); textInput.defaultValue = innerPropertiesList[i][key].value; textInput.placeholder = innerPropertiesList[i][key].name; textInput.label = innerPropertiesList[i][key].name; textInput.onValueChanged = (sender) => { innerPropertiesList[i][key].value = sender.value; this.collectionChanged(context, innerPropertiesList, false); }; textInput.spacing = Adaptive.Spacing.Small; newColumn.addItem(textInput); }); columnSet.addColumn(newColumn); let removeAction = new Adaptive.SubmitAction(); removeAction.title = "X"; removeAction.tooltip = "Remove"; removeAction.onExecute = (sender) => { innerPropertiesList.splice(i, 1); this.collectionChanged(context, innerPropertiesList, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.addAction(removeAction); let removeColumn = new Adaptive.Column("auto"); removeColumn.spacing = Adaptive.Spacing.Small; removeColumn.verticalContentAlignment = Adaptive.VerticalAlignment.Center; removeColumn.addItem(actionSet); columnSet.addColumn(removeColumn); result.addItem(columnSet); } } let addAction = new Adaptive.SubmitAction(); addAction.title = this.addButtonTitle; addAction.onExecute = (sender) => { innerPropertiesList.push({...this.innerPropertiesDefaults}); this.collectionChanged(context, innerPropertiesList, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.spacing = Adaptive.Spacing.Small; actionSet.addAction(addAction); result.addItem(actionSet); return result; } constructor( readonly targetVersion: Adaptive.TargetVersion, readonly collectionPropertyName: string, readonly innerPropertiesDefaults: InnerPropertiesDictionary, readonly createCollectionItem: (innerProperties: InnerPropertiesDictionary) => any, readonly addButtonTitle: string = "Add", readonly messageIfEmpty: string = "This collection is empty") { super(targetVersion); } } export abstract class DesignerPeer extends DraggableElement { static readonly idProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "id", "Id"); static onPopulatePropertySheet?: (sender: DesignerPeer, propertySheet: PropertySheet) => void; private _parent: DesignerPeer; private _cardObject: Adaptive.CardObject; private _children: Array = []; private _isSelected: boolean = false; private _inplaceEditorOverlay: HTMLElement; private _inplaceEditor: DesignerPeerInplaceEditor = null; private _insertAfterNeighbor = false; private _parentCarouselPagePeer: CarouselPagePeer | undefined = undefined; private closeInplaceEditor(applyChanges: boolean) { if (this._inplaceEditor) { if (applyChanges) { this._inplaceEditor.applyChanges(); this.changed(true); } this._inplaceEditor = null; this._inplaceEditorOverlay.parentNode.removeChild(this._inplaceEditorOverlay); } } private tryOpenInplaceEditor(): boolean { this._inplaceEditor = this.createInplaceEditor(); if (this._inplaceEditor) { this._inplaceEditor.onClose = (applyChanges: boolean) => { this.closeInplaceEditor(applyChanges); }; this._inplaceEditorOverlay = document.createElement("div"); this._inplaceEditorOverlay.tabIndex = 0; this._inplaceEditorOverlay.style.zIndex = "600"; this._inplaceEditorOverlay.style.backgroundColor = "transparent"; this._inplaceEditorOverlay.style.position = "absolute"; this._inplaceEditorOverlay.style.left = "0"; this._inplaceEditorOverlay.style.top = "0"; this._inplaceEditorOverlay.style.width = document.documentElement.scrollWidth + "px"; this._inplaceEditorOverlay.style.height = document.documentElement.scrollHeight + "px"; this._inplaceEditorOverlay.onfocus = (e) => { this.closeInplaceEditor(true); }; let cardObjectBoundingRect = this.getCardObjectBoundingRect(); let peerBoundingRect = this.getBoundingRect(); let topPadding = peerBoundingRect.height - cardObjectBoundingRect.height; let inplaceEditorHost = document.createElement("div"); inplaceEditorHost.className = "acd-inplace-editor-host"; inplaceEditorHost.style.left = Math.floor(cardObjectBoundingRect.left + pageXOffset) + "px"; inplaceEditorHost.style.top = Math.floor(cardObjectBoundingRect.top + pageYOffset - topPadding) + "px"; inplaceEditorHost.style.width = Math.ceil(peerBoundingRect.width) + "px"; inplaceEditorHost.style.height = Math.ceil(peerBoundingRect.height) + "px"; inplaceEditorHost.style.paddingTop = topPadding + "px"; let renderedInplaceEditor = this._inplaceEditor.render(); renderedInplaceEditor.classList.add("acd-inplace-editor"); renderedInplaceEditor.tabIndex = 0; renderedInplaceEditor.onblur = (e) => { this.closeInplaceEditor(true); }; inplaceEditorHost.appendChild(renderedInplaceEditor); this._inplaceEditorOverlay.appendChild(inplaceEditorHost); document.body.appendChild(this._inplaceEditorOverlay); this._inplaceEditor.initialize(); return true; } return false; } protected click(e: MouseEvent) { super.click(e); this.isSelected = true; } protected doubleClick(e: MouseEvent) { super.doubleClick(e); this.tryOpenInplaceEditor(); } protected isContainer(): boolean { return false; } protected getToolTip(): string { return null; } protected internalAddCommands(context: DesignContext, commands: Array) { // Do nothing in base implementation } protected internalRender(): HTMLElement { let element = document.createElement("div"); element.classList.add("acd-peer"); element.tabIndex = 0; element.onfocus = (e) => { this.isSelected = true; }; let toolTip = this.getToolTip(); if (toolTip) { element.title = toolTip; } if (this.isContainer()) { element.classList.add("container"); } element.style.position = "absolute"; // Issue: I believe this stops us from interacting with the elements - this could be okay since it shouldn't be an issue in preview mode // Ensure that the peer is in front of the card element.style.zIndex = "10"; return element; } protected internalUpdateCssStyles() { if (this.isSelected) { this.renderedElement.classList.add("selected"); } else { this.renderedElement.classList.remove("selected"); } if (this.dragging) { this.renderedElement.classList.add("dragging"); } else { this.renderedElement.classList.remove("dragging"); } } protected peerAdded(newPeer: DesignerPeer) { this.changed(false); if (this.onPeerAdded) { this.onPeerAdded(this, newPeer); } } protected peerRemoved(peer: DesignerPeer) { if (this.onPeerRemoved) { this.onPeerRemoved(peer); } } protected internalUpdateLayout() { if (this.renderedElement) { let clientRect = this.getBoundingRect(); this.renderedElement.style.width = clientRect.width + "px"; this.renderedElement.style.height = clientRect.height + "px"; this.renderedElement.style.left = clientRect.left + "px"; this.renderedElement.style.top = clientRect.top + "px"; } this.updateAriaProperties(); } protected updateAriaProperties() { if (this._children.length === 0 && this.getCardObject() instanceof Adaptive.CardElementContainer) { this.renderedElement.setAttribute("aria-label", "Empty " + this.getCardObject().getJsonTypeName()); } else { this.renderedElement.setAttribute("aria-label", this.getCardObject().getJsonTypeName()); } } protected createInplaceEditor(): DesignerPeerInplaceEditor { return null; } protected internalGetTreeItemText(): string { return null; } protected abstract internalRemove(): boolean; readonly registration: DesignerPeerRegistrationBase; readonly designerSurface: CardDesignerSurface; readonly treeItem: DesignerPeerTreeItem; onParentChanged: (sender: DesignerPeer) => void; onSelectedChanged: (sender: DesignerPeer) => void; onChanged: (sender: DesignerPeer, updatePropertySheet: boolean) => void; onPeerRemoved: (sender: DesignerPeer) => void; onPeerAdded: (sender: DesignerPeer, newPeer: DesignerPeer) => void; constructor( parent: DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeerRegistrationBase, cardObject: Adaptive.CardObject) { super(); this._parent = parent; this.registration = registration; this.designerSurface = designerSurface; this._cardObject = cardObject; this.treeItem = new DesignerPeerTreeItem(this, this.isTreeItemExpandedByDefault); } abstract getBoundingRect(): Rect; abstract getCardObjectBoundingRect(): Rect; getCardObject(): Adaptive.CardObject { return this._cardObject; } updateChildren() { for (let i = 0; i < this.getChildCount(); i++) { this.getChildAt(i).updateChildren(); } } changed(updatePropertySheet: boolean) { if (this.onChanged) { this.onChanged(this, updatePropertySheet); } } getTreeItemText(): string { return this.internalGetTreeItemText(); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { // Do nothing in base implementation } canDrop(peer: DesignerPeer): boolean { return false; } canBeRemoved(): boolean { return true; } tryDrop(peer: DesignerPeer, insertionPoint: IPoint): boolean { return false; } tryAdd(peer: DesignerPeer): boolean { return false; } insertChild(peer: DesignerPeer, index: number = -1) { if (index == -1) { this._children.push(peer); } else { this._children.splice(index, 0, peer); } peer.parent = this; this.peerAdded(peer); } removeChild(peer: DesignerPeer) { var index = this._children.indexOf(peer); if (index >= 0) { peer.parent = null; this._children.splice(index, 1); } } getChildCount(): number { return this._children.length; } getChildAt(index: number): DesignerPeer { return this._children[index]; } getCommands(context: DesignContext, promoteParentCommands: boolean = false): Array { let result: Array = []; this.internalAddCommands(context, result); if (promoteParentCommands && this.parent) { let parentCommands = this.parent.getCommands(context); for (let command of parentCommands) { if (command.isPromotable) { result.push(command); } } } return result; } remove(onlyFromCard: boolean, removeChildren: boolean): boolean { if (removeChildren) { while (this._children.length > 0) { this._children[0].remove(onlyFromCard, removeChildren); } } var result = this.internalRemove(); if (result && !onlyFromCard) { if (this.parent) { this.parent.removeChild(this); } this.removeElementsFromDesignerSurface(); this.peerRemoved(this); } return result; } addElementsToDesignerSurface(designerSurface: HTMLElement, neighbor: HTMLElement = undefined): HTMLElement { if (this.renderedElement) { if (neighbor) { // Adds the rendered element after its neighbor neighbor.after(this.renderedElement); if (this.getChildCount() >= 0) { neighbor = this.renderedElement; for (let i = 0; i < this.getChildCount(); i++) { // We need to update the neighbor with the most recently added element neighbor = this.getChildAt(i).addElementsToDesignerSurface(designerSurface, neighbor); } } } else { // The first time we render the card, we can append the elements in order designerSurface.appendChild(this.renderedElement); } } return neighbor; } removeElementsFromDesignerSurface(processChildren: boolean = false) { this.renderedElement.parentNode.removeChild(this.renderedElement); if (processChildren) { for (let i = 0; i < this.getChildCount(); i++) { this.getChildAt(i).removeElementsFromDesignerSurface(processChildren); } } } buildPropertySheetCard(context: DesignContext): Adaptive.AdaptiveCard { let card = new Adaptive.AdaptiveCard(); card.padding = new Adaptive.PaddingDefinition( Adaptive.Spacing.Small, Adaptive.Spacing.Small, Adaptive.Spacing.Small, Adaptive.Spacing.Small); let propertySheet = new PropertySheet(); this.populatePropertySheet(propertySheet); if (DesignerPeer.onPopulatePropertySheet) { DesignerPeer.onPopulatePropertySheet(this, propertySheet); } propertySheet.render( card, new PropertySheetContext(context, this)); let actionSet = new Adaptive.ActionSet(); let commands = this.getCommands(context, true); for (let command of commands) { if (command.showInPropertySheet) { let action = new Adaptive.SubmitAction(); action.title = command.name; action.onExecute = (sender: Adaptive.Action) => { command.execute(command, action.renderedElement); }; actionSet.addAction(action); } } actionSet.separator = true; card.addItem(actionSet); return card; } scrollIntoView() { if (this.renderedElement) { this.renderedElement.scrollIntoView(); } if (this.treeItem && this.treeItem.renderedElement) { this.treeItem.renderedElement.scrollIntoView(); } } private updateParentCarouselPagePeer(value: CarouselPagePeer | undefined) { // If the value has changed, we should update it and the current peer's children if (value !== this._parentCarouselPagePeer) { this._parentCarouselPagePeer = value; for (const child of this._children) { child.updateParentCarouselPagePeer(this._parentCarouselPagePeer); } } } get isTreeItemExpandedByDefault(): boolean { return true; } get parent(): DesignerPeer { return this._parent; } set parent(value: DesignerPeer) { this._parent = value; this.updateParentCarouselPagePeer(this._parent instanceof CarouselPagePeer ? this._parent : this._parent?._parentCarouselPagePeer); if (this.onParentChanged) { this.onParentChanged(this); } } get isSelected(): boolean { return this._isSelected; } set isSelected(value: boolean) { if (value != this._isSelected) { this._isSelected = value; this.updateLayout(); this.treeItem.isSelected = this._isSelected; if (this.onSelectedChanged) { this.onSelectedChanged(this); } } } get insertAfterNeighbor(): boolean { return this._insertAfterNeighbor; } set insertAfterNeighbor(insertAfterNeighbor: boolean) { this._insertAfterNeighbor = insertAfterNeighbor; } get children(): Array { return this._children; } get parentCarouselPagePeer(): CarouselPagePeer | undefined { return this._parentCarouselPagePeer; } } export class ActionPeer extends DesignerPeer { static readonly titleProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "title", "Title", true); static readonly modeProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_5, "mode", "Mode", [ { targetVersion: Adaptive.Versions.v1_5, name: "Primary", value: Adaptive.ActionMode?.Primary }, { targetVersion: Adaptive.Versions.v1_5, name: "Secondary", value: Adaptive.ActionMode?.Secondary } ]); static readonly styleProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_2, "style", "Style", [ { targetVersion: Adaptive.Versions.v1_2, name: "Default", value: Adaptive.ActionStyle?.Default }, { targetVersion: Adaptive.Versions.v1_2, name: "Positive", value: Adaptive.ActionStyle?.Positive }, { targetVersion: Adaptive.Versions.v1_2, name: "Destructive", value: Adaptive.ActionStyle?.Destructive } ]); static readonly iconUrlProperty = new StringPropertyEditor(Adaptive.Versions.v1_1, "iconUrl", "Icon URL"); static readonly tooltipProperty = new StringPropertyEditor(Adaptive.Versions.v1_5, "tooltip", "Tooltip"); static readonly isEnabledProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_5, "isEnabled", "Enabled"); static readonly roleProperty = new EnumPropertyEditor(Adaptive.Versions.v1_6, "role", "Role", Adaptive.ActionRole); protected doubleClick(e: MouseEvent) { super.doubleClick(e); this.action.renderedElement.click(); } protected internalRemove(): boolean { return this.action.remove(); } constructor( parent: DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeerRegistrationBase, action: Adaptive.Action) { super(parent, designerSurface, registration, action); } protected internalGetTreeItemText(): string { if (this.action.title && this.action.title != "") { return this.action.title; } else { return super.internalGetTreeItemText(); } } protected internalUpdateLayout() { if (this.action.renderedElement) { super.internalUpdateLayout(); } } isDraggable(): boolean { return false; } getBoundingRect(): Rect { let designSurfaceOffset = this.designerSurface.getDesignerSurfaceOffset(); let actionBoundingRect = this.action.renderedElement.getBoundingClientRect(); return new Rect( actionBoundingRect.top - designSurfaceOffset.y, actionBoundingRect.right - designSurfaceOffset.x, actionBoundingRect.bottom - designSurfaceOffset.y, actionBoundingRect.left - designSurfaceOffset.x ); } getCardObjectBoundingRect(): Rect { let actionBoundingRect = this.action.renderedElement.getBoundingClientRect(); return new Rect( actionBoundingRect.top, actionBoundingRect.right, actionBoundingRect.bottom, actionBoundingRect.left ); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ActionPeer.idProperty, ActionPeer.isEnabledProperty, ActionPeer.titleProperty, ActionPeer.tooltipProperty, ActionPeer.modeProperty, ActionPeer.styleProperty, ActionPeer.iconUrlProperty, ActionPeer.roleProperty); } get action(): Adaptive.Action { return this.getCardObject(); } } export abstract class TypedActionPeer extends ActionPeer { constructor( parent: DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeerRegistrationBase, action: TAction) { super(parent, designerSurface, registration, action); } get action(): TAction { return this.getCardObject(); } } export class HttpActionPeer extends TypedActionPeer { static readonly ignoreInputValidationProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_3, "ignoreInputValidation", "Ignore input validation"); static readonly methodProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_0, "method", "Method", [ { targetVersion: Adaptive.Versions.v1_0, name: "GET", value: "GET" }, { targetVersion: Adaptive.Versions.v1_0, name: "POST", value: "POST" } ], true); static readonly urlProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "url", "Url"); static readonly bodyProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "body", "Body", false, true); static readonly headersProperty = new NameValuePairPropertyEditor( Adaptive.Versions.v1_0, "headers", "name", "value", (name: string, value: string) => { return new Adaptive.HttpHeader(name, value); }, "Name", "Value", "Add a new header", "This action has no header."); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( PropertySheetCategory.DefaultCategory, HttpActionPeer.ignoreInputValidationProperty); propertySheet.add( defaultCategory, HttpActionPeer.methodProperty, HttpActionPeer.urlProperty); if (this.action.method == "POST") { propertySheet.add( defaultCategory, HttpActionPeer.bodyProperty); } propertySheet.add( "HTTP headers", HttpActionPeer.headersProperty); } } export abstract class BaseSubmitActionPeer extends TypedActionPeer { static readonly dataProperty = new ObjectPropertyEditor(Adaptive.Versions.v1_0, "data", "Data"); static readonly associatedInputsProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_3, "associatedInputs", "Associated inputs", [ { targetVersion: Adaptive.Versions.v1_3, name: "Automatic", value: "auto" }, { targetVersion: Adaptive.Versions.v1_3, name: "None", value: "none" } ]); static readonly disabledUnlessAssociatedInputsChangeProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_6, "disabledUnlessAssociatedInputsChange", "Only enable when associated inputs change"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, BaseSubmitActionPeer.dataProperty, BaseSubmitActionPeer.associatedInputsProperty, BaseSubmitActionPeer.disabledUnlessAssociatedInputsChangeProperty); } } export class SubmitActionPeer extends BaseSubmitActionPeer { } export class ExecuteActionPeer extends BaseSubmitActionPeer { static readonly verbProperty = new StringPropertyEditor(Adaptive.Versions.v1_4, "verb", "Verb"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add(defaultCategory, ExecuteActionPeer.verbProperty); } } export class OpenUrlActionPeer extends TypedActionPeer { static readonly urlProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "url", "Url"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, OpenUrlActionPeer.urlProperty); } } export class ShowCardActionPeer extends TypedActionPeer { protected getToolTip(): string { return "Double click to open/close"; } } export class ToggleVisibilityActionPeer extends TypedActionPeer { } export class CardElementPeer extends DesignerPeer { static readonly dataContextProperty = new CustomCardObjectPropertyEditor("*", "$data", "Data context", true); static readonly whenProperty = new CustomCardObjectPropertyEditor("*", "$when", "Only show when", true); static readonly idProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "id", "Id"); static readonly isVisibleProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_2, "isVisible", "Initially visible"); static readonly spacingProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "spacing", "Spacing", Adaptive.Spacing); static readonly separatorProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_0, "separator", "Separator"); static readonly horizontalAlignmentProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "horizontalAlignment", "Horizontal alignment", Adaptive.HorizontalAlignment); static readonly heightProperty = new HeightPropertyEditor( Adaptive.Versions.v1_1, "height", "Height", [ { targetVersion: Adaptive.Versions.v1_1, name: "Automatic", value: "auto" }, { targetVersion: Adaptive.Versions.v1_1, name: "Stretch", value: "stretch" } ] ); protected insertElementAfter(newElement: Adaptive.CardElement) { if (this.cardElement.parent instanceof Adaptive.Container) { this.cardElement.parent.insertItemAfter(newElement, this.cardElement); var newPeer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance(this.designerSurface, this, newElement); this.peerAdded(newPeer); } } protected internalRemove(): boolean { return this.cardElement.remove(); } protected internalUpdateCssStyles() { super.internalUpdateCssStyles(); if (this.cardElement.isVisible) { this.renderedElement.classList.remove("invisible"); } else { this.renderedElement.classList.add("invisible"); } } constructor( parent: DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeerRegistrationBase, cardElement: Adaptive.CardElement, initializeCardElement?: boolean) { super(parent, designerSurface, registration, cardElement); if (initializeCardElement === true) { this.initializeCardElement(); } this.updateChildren(); } findCardElementChild(element: Adaptive.CardElement) : CardElementPeer | undefined { for (let i = 0; i < this.getChildCount(); i++) { let peer = this.getChildAt(i); if (peer instanceof CardElementPeer && peer.cardElement === element) { return peer; } } return undefined; } findActionChild(action: Adaptive.Action) : ActionPeer | undefined { for (let i = 0; i < this.getChildCount(); i++) { let peer = this.getChildAt(i); if (peer instanceof ActionPeer && peer.action === action) { return peer; } } return undefined; } updateChildren(initializeCardElement?: boolean) { // Remove all peers from the designer surface for (let i = 0; i < this.getChildCount(); i++) { let existingPeer = this.getChildAt(i); if (existingPeer instanceof CardElementPeer && this.cardElement.indexOf(existingPeer.cardElement) < 0) { this.removeChild(existingPeer); } if (existingPeer instanceof ActionPeer && this.cardElement.indexOfAction(existingPeer.action) < 0) { this.removeChild(existingPeer); } } let itemCount = 0; if (this.cardElement instanceof Adaptive.CardElementContainer) { itemCount = this.cardElement.getItemCount(); for (let i = 0; i < itemCount; i++) { let existingPeer = this.findCardElementChild(this.cardElement.getItemAt(i)); if (!existingPeer) { const peer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance( this.designerSurface, this, this.cardElement.getItemAt(i), initializeCardElement); this.insertChild(peer, i); } } } for (let i = 0; i < this.cardElement.getActionCount(); i++) { let existingPeer = this.findActionChild(this.cardElement.getActionAt(i)); if (!existingPeer) { this.insertChild( CardDesignerSurface.actionPeerRegistry.createPeerInstance( this.designerSurface, this, this.cardElement.getActionAt(i)), itemCount + i); } } super.updateChildren(); } getTreeItemText(): string { let text = super.getTreeItemText(); if (this.cardElement.isVisible) { return text; } else { let result = "Hidden"; if (text) { result += " - " + text; } return result; } } initializeCardElement() { // Do nothing in base implementation } canDrop(peer: DesignerPeer) { let parent = this.parent; let parentCanContainElement = true; while (parent) { if (parent instanceof CarouselPagePeer) { parentCanContainElement = parent.canDrop(peer); break; } parent = parent.parent; } return this.cardElement instanceof Adaptive.Container && peer instanceof CardElementPeer && parentCanContainElement; } tryDrop(peer: DesignerPeer, insertionPoint: IPoint): boolean { // Even though Carousel is a Container, we do not want to be able to drop peers if (this.cardElement instanceof Adaptive.Container && peer instanceof CardElementPeer && !(this.cardElement instanceof Adaptive.Carousel)) { let targetChild: DesignerPeer = null; let insertAfter: boolean; for (let i = 0; i < this.getChildCount(); i++) { let rect = this.getChildAt(i).getBoundingRect(); if (rect.isInside(insertionPoint)) { targetChild = this.getChildAt(i); insertAfter = (insertionPoint.y - rect.top) >= (rect.height / 2); break; } } if (targetChild != peer) { if (peer.cardElement.parent) { if (!peer.remove(true, false)) { return false; } peer.parent.removeChild(peer); } if (!targetChild) { let rect = this.getBoundingRect(); insertAfter = (insertionPoint.y - rect.top) >= (rect.height / 2); if (this.cardElement.getItemCount() > 0 && insertAfter) { this.cardElement.insertItemAfter(peer.cardElement, this.cardElement.getItemAt(this.cardElement.getItemCount() - 1)); } else { this.cardElement.insertItemAfter(peer.cardElement, null); } } else { if (insertAfter) { this.cardElement.insertItemAfter(peer.cardElement, (targetChild).cardElement); } else { this.cardElement.insertItemBefore(peer.cardElement, (targetChild).cardElement); } } this.insertChild(peer, peer.cardElement.index); this.changed(false); return true; } } return false; } tryAdd(peer: DesignerPeer): boolean { if (this.cardElement instanceof Adaptive.Container && peer instanceof CardElementPeer) { if (peer.cardElement.parent) { if (!peer.remove(true, false)) { return false; } peer.parent.removeChild(peer); } this.cardElement.addItem(peer.cardElement); this.insertChild(peer, peer.cardElement.index); this.changed(false); return true; } return false; } getBoundingRect(): Rect { let designSurfaceOffset = this.designerSurface.getDesignerSurfaceOffset(); let cardElementBoundingRect = this.cardElement.renderedElement.getBoundingClientRect(); let returnRect = undefined; if (this.cardElement.hasVisibleSeparator) { let separatorBoundingRect = this.cardElement.separatorElement.getBoundingClientRect(); returnRect = new Rect( Math.min(separatorBoundingRect.top, cardElementBoundingRect.top) - designSurfaceOffset.y, Math.max(separatorBoundingRect.right, cardElementBoundingRect.right) - designSurfaceOffset.x, Math.max(separatorBoundingRect.bottom, cardElementBoundingRect.bottom) - designSurfaceOffset.y, Math.min(separatorBoundingRect.left, cardElementBoundingRect.left) - designSurfaceOffset.x ); } else { returnRect = new Rect( cardElementBoundingRect.top - designSurfaceOffset.y, cardElementBoundingRect.right - designSurfaceOffset.x, cardElementBoundingRect.bottom - designSurfaceOffset.y, cardElementBoundingRect.left - designSurfaceOffset.x ); } // If we are displaying a carousel, we need to make sure that all of the child peers are in the correct location // First check if the peer has a parent that is a CarouselPagePeer and it is the visible page // Then see if the bounding rect needs to be adjusted if (this.parentCarouselPagePeer?.isVisible() && !this.parentCarouselPagePeer.peerInCorrectLocation({ x: returnRect.x, y: returnRect.y })) { returnRect.left = returnRect.left - this.parentCarouselPagePeer.pageOffset; returnRect.right = returnRect.right - this.parentCarouselPagePeer.pageOffset; } return returnRect; } getCardObjectBoundingRect(): Rect { let cardElementBoundingRect = this.cardElement.renderedElement.getBoundingClientRect(); return new Rect( cardElementBoundingRect.top, cardElementBoundingRect.right, cardElementBoundingRect.bottom, cardElementBoundingRect.left ); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); if (GlobalSettings.enableDataBindingSupport) { propertySheet.add( defaultCategory, CardElementPeer.dataContextProperty, CardElementPeer.whenProperty); } propertySheet.add( defaultCategory, CardElementPeer.idProperty, CardElementPeer.isVisibleProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, CardElementPeer.spacingProperty, CardElementPeer.separatorProperty, CardElementPeer.horizontalAlignmentProperty, CardElementPeer.heightProperty); } get cardElement(): Adaptive.CardElement { return this.getCardObject(); } isVisible(): boolean { if (this.parent instanceof CardElementPeer) { return this.parent.isVisible(); } return true; } bringCardElementIntoView(): boolean { return false; } } export abstract class TypedCardElementPeer extends CardElementPeer { constructor( parent: DesignerPeer, designerSurface: CardDesignerSurface, registration: DesignerPeerRegistrationBase, cardElement: TCardElement, initializeCardElement?: boolean) { super(parent, designerSurface, registration, cardElement, initializeCardElement); } get cardElement(): TCardElement { return this.getCardObject(); } } export class AdaptiveCardPeer extends TypedCardElementPeer { static readonly langProperty = new StringPropertyEditor(Adaptive.Versions.v1_1, "lang", "Language"); static readonly fallbackTextProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "fallbackText", "Fallback text", false, true); static readonly speakProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "speak", "Speak"); static readonly refreshProperty = new CompoundPropertyEditor( Adaptive.Versions.v1_4, "refresh", [ new StringArrayPropertyEditor(Adaptive.Versions.v1_4, "userIds", "User IDs", false, true), new ActionPropertyEditor(Adaptive.Versions.v1_4, "action", "Action", [ "*", "-Action.Execute" ], true) ], () => { return new Adaptive.RefreshDefinition() }); protected addAction(action: Adaptive.Action) { this.cardElement.addAction(action); this.insertChild(CardDesignerSurface.actionPeerRegistry.createPeerInstance(this.designerSurface, this, action)); } protected internalRemove(): boolean { return true; } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); let availableActions: Adaptive.ITypeRegistration[] = []; for (let i = 0; i < context.hostContainer.actionsRegistry.getItemCount(); i++) { let typeRegistration = context.hostContainer.actionsRegistry.getItemAt(i); if (typeRegistration.schemaVersion.compareTo(context.targetVersion) <= 0) { availableActions.push(typeRegistration); } } if (availableActions.length > 0) { commands.push( new PeerCommand( { name: "Add an action", alwaysShowName: true, iconClass: "acd-icon-bolt", showInPropertySheet: true, execute: (command: PeerCommand, clickedElement: HTMLElement) => { let popupMenu = new Controls.PopupMenu(); for (let i = 0; i < availableActions.length; i++) { let menuItem = new Controls.DropDownItem(i.toString(), availableActions[i].typeName); menuItem.onClick = (clickedItem: Controls.DropDownItem) => { let registration = availableActions[i]; let action = new registration.objectType(); action.title = registration.typeName; this.addAction(action); popupMenu.closePopup(false); this.renderedElement.focus(); }; popupMenu.items.add(menuItem); } popupMenu.popup(clickedElement); } }) ); } } isDraggable(): boolean { return false; } canBeRemoved(): boolean { return false; } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.remove( DesignerPeer.idProperty, CardElementPeer.isVisibleProperty, CardElementPeer.horizontalAlignmentProperty, CardElementPeer.separatorProperty, CardElementPeer.heightProperty, CardElementPeer.spacingProperty); propertySheet.add( defaultCategory, AdaptiveCardPeer.langProperty, AdaptiveCardPeer.fallbackTextProperty, AdaptiveCardPeer.speakProperty); propertySheet.add( PropertySheetCategory.Refresh, AdaptiveCardPeer.refreshProperty); if (this.cardElement.refresh && this.cardElement.refresh.action) { propertySheet.addActionProperties( Adaptive.Versions.v1_4, this, this.cardElement.refresh.action, PropertySheetCategory.Refresh, [ BaseSubmitActionPeer.associatedInputsProperty, ActionPeer.iconUrlProperty, ActionPeer.styleProperty, ActionPeer.modeProperty ]); } propertySheet.add( PropertySheetCategory.LayoutCategory, ContainerPeer.minHeightProperty, ContainerPeer.verticalContentAlignmentProperty, ContainerPeer.rtlProperty); propertySheet.add( "Background image", ContainerPeer.backgroundImageProperty); propertySheet.add( PropertySheetCategory.SelectionAction, ContainerPeer.selectActionProperty); if (this.cardElement.selectAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_0, this, this.cardElement.selectAction, PropertySheetCategory.SelectionAction); } } } export class ColumnPeer extends TypedCardElementPeer { private static readonly pixelWidthProperty = new SizeAndUnitPropertyEditor(Adaptive.Versions.v1_1, "width", "Width in pixels", Adaptive.SizeUnit.Pixel); private static readonly weightProperty = new SizeAndUnitPropertyEditor(Adaptive.Versions.v1_0, "width", "Weight", Adaptive.SizeUnit.Weight); static readonly widthProperty = new ColumnWidthPropertyEditor( Adaptive.Versions.v1_0, "width", "Width", [ { targetVersion: Adaptive.Versions.v1_0, name: "Automatic", value: "auto" }, { targetVersion: Adaptive.Versions.v1_0, name: "Stretch", value: "stretch" }, { targetVersion: Adaptive.Versions.v1_0, name: "Weighted", value: "weighted" }, { targetVersion: Adaptive.Versions.v1_1, name: "Pixels", value: "pixels" } ], true); protected isContainer(): boolean { return true; } protected internalGetTreeItemText(): string { if (this.cardElement.width instanceof Adaptive.SizeAndUnit) { switch (this.cardElement.width.unit) { case Adaptive.SizeUnit.Weight: return "Weight: " + this.cardElement.width.physicalSize; default: return this.cardElement.width.physicalSize + " pixels"; } } else { switch (this.cardElement.width) { case "stretch": return "Stretch"; case "auto": return "Automatic"; default: return ""; } } } isDraggable(): boolean { return false; } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( PropertySheetCategory.LayoutCategory, ColumnPeer.widthProperty); if (this.cardElement.width instanceof Adaptive.SizeAndUnit) { if (this.cardElement.width.unit == Adaptive.SizeUnit.Pixel) { propertySheet.add( PropertySheetCategory.LayoutCategory, ColumnPeer.pixelWidthProperty); } else { propertySheet.add( PropertySheetCategory.LayoutCategory, ColumnPeer.weightProperty); } } propertySheet.add( PropertySheetCategory.LayoutCategory, ContainerPeer.minHeightProperty, ContainerPeer.verticalContentAlignmentProperty, ContainerPeer.rtlProperty); propertySheet.add( PropertySheetCategory.StyleCategory, ContainerPeer.styleProperty, ContainerPeer.bleedProperty); propertySheet.add( "Background image", ContainerPeer.backgroundImageProperty); propertySheet.add( PropertySheetCategory.SelectionAction, ContainerPeer.selectActionProperty); if (this.cardElement.selectAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_0, this, this.cardElement.selectAction, PropertySheetCategory.SelectionAction); } } } export class ColumnSetPeer extends TypedCardElementPeer { protected isContainer(): boolean { return true; } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); commands.push( new PeerCommand( { name: "Add a column", iconClass: "acd-icon-addColumn", isPromotable: true, execute: (command: PeerCommand, clickedElement: HTMLElement) => { var column = new Adaptive.Column(); column.width = "stretch"; this.cardElement.addColumn(column); const newPeer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance(this.designerSurface, this, column); newPeer.insertAfterNeighbor = true; this.insertChild(newPeer); } }) ); } protected internalGetTreeItemText(): string { let columnCount = this.cardElement.getItemCount(); switch (columnCount) { case 0: return "No column"; case 1: return "1 column"; default: return columnCount + " columns"; } } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ContainerPeer.minHeightProperty, ContainerPeer.styleProperty, ContainerPeer.bleedProperty); propertySheet.add( PropertySheetCategory.SelectionAction, ContainerPeer.selectActionProperty); if (this.cardElement.selectAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_0, this, this.cardElement.selectAction, PropertySheetCategory.SelectionAction); } } canDrop(peer: DesignerPeer) { return true; } } export class ContainerPeer extends TypedCardElementPeer { static readonly selectActionProperty = new ActionPropertyEditor(Adaptive.Versions.v1_1, "selectAction", "Action type", [ Adaptive.ShowCardAction.JsonTypeName ], true); static readonly minHeightProperty = new NumberPropertyEditor(Adaptive.Versions.v1_2, "minPixelHeight", "Minimum height in pixels"); static readonly verticalContentAlignmentProperty = new EnumPropertyEditor(Adaptive.Versions.v1_1, "verticalContentAlignment", "Vertical content alignment", Adaptive.VerticalAlignment); static readonly styleProperty = new ContainerStylePropertyEditor(Adaptive.Versions.v1_0, "style", "Style"); static readonly bleedProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_2, "bleed", "Bleed"); static readonly backgroundImageProperty = new CompoundPropertyEditor( Adaptive.Versions.v1_0, "backgroundImage", [ new StringPropertyEditor(Adaptive.Versions.v1_0, "url", "URL", true), new EnumPropertyEditor(Adaptive.Versions.v1_2, "fillMode", "Fill mode", Adaptive.FillMode), new EnumPropertyEditor(Adaptive.Versions.v1_2, "horizontalAlignment", "Horizontal alignment", Adaptive.HorizontalAlignment), new EnumPropertyEditor(Adaptive.Versions.v1_2, "verticalAlignment", "Vertical alignment", Adaptive.VerticalAlignment) ] ); static readonly rtlProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_5, "rtl", "Present right to left"); protected isContainer(): boolean { return true; } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( PropertySheetCategory.LayoutCategory, ContainerPeer.minHeightProperty, ContainerPeer.verticalContentAlignmentProperty, ContainerPeer.rtlProperty); propertySheet.add( PropertySheetCategory.StyleCategory, ContainerPeer.styleProperty, ContainerPeer.bleedProperty); propertySheet.add( "Background image", ContainerPeer.backgroundImageProperty); propertySheet.add( PropertySheetCategory.SelectionAction, ContainerPeer.selectActionProperty); if (this.cardElement.selectAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_0, this, this.cardElement.selectAction, PropertySheetCategory.SelectionAction); } } } export class ActionSetPeer extends TypedCardElementPeer { protected addAction(action: Adaptive.Action) { this.cardElement.addAction(action); const newPeer = CardDesignerSurface.actionPeerRegistry.createPeerInstance(this.designerSurface, this, action); newPeer.insertAfterNeighbor = true; this.insertChild(newPeer); } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); let availableActions: Adaptive.ITypeRegistration[] = []; for (let i = 0; i < context.hostContainer.actionsRegistry.getItemCount(); i++) { let typeRegistration = context.hostContainer.actionsRegistry.getItemAt(i); if (typeRegistration.schemaVersion.compareTo(context.targetVersion) <= 0) { availableActions.push(typeRegistration); } } if (availableActions.length > 0) { commands.push( new PeerCommand( { name: "Add an action", alwaysShowName: true, iconClass: "acd-icon-bolt", showInPropertySheet: true, execute: (command: PeerCommand, clickedElement: HTMLElement) => { let popupMenu = new Controls.PopupMenu(); for (let i = 0; i < availableActions.length; i++) { let menuItem = new Controls.DropDownItem(i.toString(), availableActions[i].typeName); menuItem.onClick = (clickedItem: Controls.DropDownItem) => { let registration = availableActions[i]; let action = new registration.objectType(); action.title = registration.typeName; this.addAction(action); popupMenu.closePopup(false); }; popupMenu.items.add(menuItem); } popupMenu.popup(clickedElement); } }) ); } } } export class ImageSetPeer extends TypedCardElementPeer { static readonly ImageSizeProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "imageSize", "Image size", Adaptive.ImageSize); protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); commands.push( new PeerCommand( { name: "Add an image", iconClass: "acd-icon-image", isPromotable: true, execute: (command: PeerCommand, clickedElement: HTMLElement) => { let newImage = new Adaptive.Image(); this.cardElement.addImage(newImage); const newPeer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance(this.designerSurface, this, newImage); newPeer.insertAfterNeighbor = true; this.insertChild(newPeer); } }) ); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ImageSetPeer.ImageSizeProperty); } } export class ImagePeer extends TypedCardElementPeer { static readonly urlProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "url", "Url", true); static readonly altTextProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "altText", "Alternate text", true); static readonly sizeProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "size", "Size", Adaptive.Size); static readonly pixelWidthProperty = new NumberPropertyEditor(Adaptive.Versions.v1_1, "pixelWidth", "Width in pixels"); static readonly pixelHeightProperty = new NumberPropertyEditor(Adaptive.Versions.v1_1, "pixelHeight", "Height in pixels"); static readonly styleProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "style", "Style", Adaptive.ImageStyle); static readonly backgroundColorProperty = new StringPropertyEditor(Adaptive.Versions.v1_1, "backgroundColor", "Background color"); private get isParentImageSet(): boolean { return this.parent && this.parent instanceof ImageSetPeer; } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); if (GlobalSettings.enableDataBindingSupport && context.dataStructure) { commands.push( new PeerCommand( { name: "Bind...", alwaysShowName: true, toolTip: "Select a data field to bind this Image to.", execute: (command: PeerCommand, clickedElement: HTMLElement) => { let fieldPicker = new FieldPicker(context.dataStructure); fieldPicker.onClose = (sender, wasCancelled) => { if (!wasCancelled) { this.cardElement.url = fieldPicker.selectedField.asExpression(); this.changed(true); } }; fieldPicker.popup(clickedElement); } }) ); } } isDraggable(): boolean { return !this.isParentImageSet; } getBoundingRect(): Rect { if (this.isParentImageSet) { let designSurfaceOffset = this.designerSurface.getDesignerSurfaceOffset(); let actionBoundingRect = this.cardElement.renderedElement.getBoundingClientRect(); let returnRect = new Rect( actionBoundingRect.top - designSurfaceOffset.y, actionBoundingRect.right - designSurfaceOffset.x, actionBoundingRect.bottom - designSurfaceOffset.y, actionBoundingRect.left - designSurfaceOffset.x ); // If we are displaying a carousel, we need to make sure that all of the child peers are in the correct location // First check if the peer has a parent that is a CarouselPagePeer and it is the visible page // Then see if the bounding rect needs to be adjusted if (this.parentCarouselPagePeer?.isVisible() && !this.parentCarouselPagePeer.peerInCorrectLocation({ x: returnRect.right, y: returnRect.bottom })) { returnRect.left = returnRect.left - this.parentCarouselPagePeer.pageOffset; returnRect.right = returnRect.right - this.parentCarouselPagePeer.pageOffset; } return returnRect; } else { return super.getBoundingRect(); } } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ImagePeer.urlProperty, ImagePeer.altTextProperty); if (!this.isParentImageSet) { propertySheet.add( PropertySheetCategory.LayoutCategory, ImagePeer.sizeProperty, ImagePeer.pixelWidthProperty, ImagePeer.pixelHeightProperty); propertySheet.add( PropertySheetCategory.StyleCategory, ImagePeer.styleProperty, ImagePeer.backgroundColorProperty); propertySheet.add( PropertySheetCategory.SelectionAction, ContainerPeer.selectActionProperty); if (this.cardElement.selectAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_0, this, this.cardElement.selectAction, PropertySheetCategory.SelectionAction); } } } } export class MediaPeer extends TypedCardElementPeer { static readonly altTextProperty = new StringPropertyEditor(Adaptive.Versions.v1_1, "altText", "Alternate text", true); static readonly posterUrlProperty = new StringPropertyEditor(Adaptive.Versions.v1_1, "posterUrl", "Poster URL", true); static readonly sourcesProperty = new InnerStructPropertyEditor( Adaptive.Versions.v1_1, "sources", {"url": {name: "URL"}, "mimeType": {name: "MIME Type"}}, (innerProperties: InnerPropertiesDictionary) => { return new Adaptive.MediaSource(innerProperties["url"].value, innerProperties["mimeType"].value); }, "Add a new source", "No source has been defined."); static readonly captionSourcesProperty = new InnerStructPropertyEditor( Adaptive.Versions.v1_6, "captionSources", {"url": {name: "URL"}, "mimeType": {name: "MIME Type"}, "label": {name: "Label"}}, (innerProperties: InnerPropertiesDictionary) => { return new Adaptive.CaptionSource(innerProperties["url"].value, innerProperties["mimeType"].value, innerProperties["label"].value); }, "Add a new caption source", "No caption source has been defined."); protected internalGetTreeItemText(): string { if (this.cardElement.selectedMediaType == "audio") { return "audio"; } else if (this.cardElement.selectedMediaType == "video") { return "video"; } else { return super.internalGetTreeItemText(); } } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, MediaPeer.altTextProperty, MediaPeer.posterUrlProperty); propertySheet.add( "Sources", MediaPeer.sourcesProperty); propertySheet.add( "Caption Sources", MediaPeer.captionSourcesProperty); } } export class FactSetPeer extends TypedCardElementPeer { static readonly factsProperty = new NameValuePairPropertyEditor( Adaptive.Versions.v1_0, "facts", "name", "value", (name: string, value: string) => { return new Adaptive.Fact(name, value); }, "Name", "Value", "Add a new fact", "This FactSet is empty."); protected internalGetTreeItemText(): string { if (this.cardElement.facts.length == 0) { return "No fact"; } let allNames = this.cardElement.facts.map( (value, index, array) => { return value.name; } ) return allNames.join(", "); } initializeCardElement() { super.initializeCardElement(); this.cardElement.facts.push( new Adaptive.Fact("Fact 1", "Value 1"), new Adaptive.Fact("Fact 2", "Value 2") ); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( "Facts", FactSetPeer.factsProperty); propertySheet.remove(CardElementPeer.horizontalAlignmentProperty); } } export abstract class InputPeer extends TypedCardElementPeer { static readonly labelProperty = new StringPropertyEditor( Adaptive.Versions.v1_3, "label", "Label"); static readonly isRequiredProperty = new BooleanPropertyEditor( Adaptive.Versions.v1_3, "isRequired", "Required"); static readonly errorMessageProperty = new StringPropertyEditor( Adaptive.Versions.v1_3, "errorMessage", "Error message"); protected internalGetTreeItemText(): string { return this.cardElement.id ? this.cardElement.id : super.internalGetTreeItemText(); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add(defaultCategory, InputPeer.labelProperty); propertySheet.add( PropertySheetCategory.Validation, InputPeer.isRequiredProperty, InputPeer.errorMessageProperty); propertySheet.remove( CardElementPeer.horizontalAlignmentProperty, CardElementPeer.heightProperty); } } export class TextInputPeer extends InputPeer { static readonly defaultValueProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly placeholderProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "placeholder", "Placeholder"); static readonly isMultilineProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_0, "isMultiline", "Multi-line", true); static readonly styleProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "style", "Style", Adaptive.InputTextStyle); static readonly maxLengthProperty = new NumberPropertyEditor(Adaptive.Versions.v1_0, "maxLength", "Maximum length"); static readonly inlineActionProperty = new ActionPropertyEditor(Adaptive.Versions.v1_2, "inlineAction", "Action type", [ Adaptive.ShowCardAction.JsonTypeName ], true); static readonly regexProperty = new StringPropertyEditor(Adaptive.Versions.v1_3, "regex", "Pattern"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, TextInputPeer.placeholderProperty, TextInputPeer.isMultilineProperty); if (!this.cardElement.isMultiline) { propertySheet.add( PropertySheetCategory.DefaultCategory, TextInputPeer.styleProperty); } else { propertySheet.add( PropertySheetCategory.LayoutCategory, CardElementPeer.heightProperty); } propertySheet.add( PropertySheetCategory.InlineAction, TextInputPeer.inlineActionProperty); if (this.cardElement.inlineAction) { propertySheet.addActionProperties( Adaptive.Versions.v1_2, this, this.cardElement.inlineAction, PropertySheetCategory.InlineAction, [ ActionPeer.styleProperty, ActionPeer.modeProperty ]); } propertySheet.add( defaultCategory, TextInputPeer.maxLengthProperty, TextInputPeer.defaultValueProperty); propertySheet.add( PropertySheetCategory.Validation, TextInputPeer.regexProperty); } initializeCardElement() { super.initializeCardElement(); this.cardElement.placeholder = "Placeholder text"; } } export class NumberInputPeer extends InputPeer { static readonly defaultValueProperty = new NumberPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly placeholderProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "placeholder", "Placeholder"); static readonly minProperty = new NumberPropertyEditor(Adaptive.Versions.v1_0, "min", "Minimum value"); static readonly maxProperty = new NumberPropertyEditor(Adaptive.Versions.v1_0, "max", "Maximum value"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, NumberInputPeer.placeholderProperty, NumberInputPeer.defaultValueProperty, NumberInputPeer.minProperty, NumberInputPeer.maxProperty); } initializeCardElement() { super.initializeCardElement(); this.cardElement.placeholder = "Placeholder text"; } } export class DateInputPeer extends InputPeer { static readonly defaultValueProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly minProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "min", "Minimum value"); static readonly maxProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "max", "Maximum value"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, DateInputPeer.defaultValueProperty, DateInputPeer.minProperty, DateInputPeer.maxProperty); } } export class TimeInputPeer extends InputPeer { static readonly defaultValueProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly minProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "min", "Minimum value"); static readonly maxProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "max", "Maximum value"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, TimeInputPeer.defaultValueProperty, TimeInputPeer.minProperty, TimeInputPeer.maxProperty); } } export class ToggleInputPeer extends InputPeer { static readonly defaultValueProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly titleProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "title", "Title", true); static readonly valueOnProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "valueOn", "Value when on"); static readonly valueOffProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "valueOff", "Value when off"); static readonly wrapProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_2, "wrap", "Wrap"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ToggleInputPeer.titleProperty, ToggleInputPeer.valueOnProperty, ToggleInputPeer.valueOffProperty, ToggleInputPeer.defaultValueProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, ToggleInputPeer.wrapProperty); } initializeCardElement() { this.cardElement.title = "New Input.Toggle"; } } export class ChoiceSetInputPeer extends InputPeer { static readonly defaultValueProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "defaultValue", "Default value"); static readonly placeholderProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "placeholder", "Placeholder"); static readonly isMultiselectProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_0, "isMultiSelect", "Allow multi selection"); static readonly styleProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_0, "style", "Style", [ { targetVersion: Adaptive.Versions.v1_0, name: "Compact", value: "compact" }, { targetVersion: Adaptive.Versions.v1_0, name: "Expanded", value: "expanded" }, { targetVersion: Adaptive.Versions.v1_5, name: "Filtered", value: "filtered" } ], true); static readonly wrapProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_2, "wrap", "Wrap"); static readonly choicesProperty = new NameValuePairPropertyEditor( Adaptive.Versions.v1_0, "choices", "title", "value", (name: string, value: string) => { return new Adaptive.Choice(name, value); }, "Title", "Value", "Add a new choice", "This ChoiceSet is empty"); populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, ChoiceSetInputPeer.placeholderProperty, ChoiceSetInputPeer.isMultiselectProperty, ChoiceSetInputPeer.styleProperty, ChoiceSetInputPeer.defaultValueProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, ToggleInputPeer.wrapProperty); propertySheet.add( "Choices", ChoiceSetInputPeer.choicesProperty); } initializeCardElement() { this.cardElement.placeholder = "Placeholder text"; this.cardElement.choices.push( new Adaptive.Choice("Choice 1", "Choice 1"), new Adaptive.Choice("Choice 2", "Choice 2") ); } } class TextBlockPeerInplaceEditor extends CardElementPeerInplaceEditor { private _renderedElement: HTMLTextAreaElement; private close(applyChanges: boolean) { if (this.onClose) { this.onClose(applyChanges); } } initialize() { this._renderedElement.select(); } applyChanges() { this.cardElement.text = this._renderedElement.value; } render() { this._renderedElement = document.createElement("textarea"); this._renderedElement.className = "acd-textBlock-inplace-editor"; this._renderedElement.value = this.cardElement.text; this._renderedElement.onkeydown = (e) => { switch (e.key) { case Controls.Constants.keys.escape: this.close(false); e.preventDefault(); e.cancelBubble = true; break; case Controls.Constants.keys.enter: this.close(true); e.preventDefault(); e.cancelBubble = true; break; } return !e.cancelBubble; }; this.cardElement.applyStylesTo(this._renderedElement); return this._renderedElement; } } export class TextBlockPeer extends TypedCardElementPeer { static readonly textProperty = new StringPropertyEditor(Adaptive.Versions.v1_0, "text", "Text", true, true); static readonly wrapProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_0, "wrap", "Wrap"); static readonly maxLinesProperty = new NumberPropertyEditor(Adaptive.Versions.v1_0, "maxLines", "Maximum lines", 0); static readonly fontTypeProperty = new EnumPropertyEditor(Adaptive.Versions.v1_2, "fontType", "Font type", Adaptive.FontType, false, true); static readonly sizeProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "size", "Size", Adaptive.TextSize, false, true); static readonly weightProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "weight", "Weight", Adaptive.TextWeight, false, true); static readonly colorProperty = new EnumPropertyEditor(Adaptive.Versions.v1_0, "color", "Color", Adaptive.TextColor, false, true); static readonly subtleProperty = new NullableBooleanPropertyEditor(Adaptive.Versions.v1_0, "isSubtle", "Subtle"); static readonly styleProperty = new ChoicePropertyEditor( Adaptive.Versions.v1_5, "style", "Base style", [ { targetVersion: Adaptive.Versions.v1_5, name: "Default", value: "default" }, { targetVersion: Adaptive.Versions.v1_5, name: "Heading", value: "heading" }, { targetVersion: Adaptive.Versions.v1_5, name: "Column header", value: "columnHeader" } ], false, true); protected createInplaceEditor(): DesignerPeerInplaceEditor { return new TextBlockPeerInplaceEditor(this.cardElement); } protected internalGetTreeItemText(): string { return this.cardElement.text; } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); if (context.dataStructure) { commands.push( new PeerCommand( { name: "Bind...", alwaysShowName: true, toolTip: "Select a data field to bind this TextBlock to.", execute: (command: PeerCommand, clickedElement: HTMLElement) => { let fieldPicker = new FieldPicker(context.dataStructure); fieldPicker.onClose = (sender, wasCancelled) => { if (!wasCancelled) { this.cardElement.text = fieldPicker.selectedField.asExpression(); this.changed(true); } }; fieldPicker.popup(clickedElement); } }) ); } } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, TextBlockPeer.textProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, TextBlockPeer.wrapProperty, TextBlockPeer.maxLinesProperty); propertySheet.add( PropertySheetCategory.StyleCategory, TextBlockPeer.styleProperty, TextBlockPeer.fontTypeProperty, TextBlockPeer.sizeProperty, TextBlockPeer.weightProperty, TextBlockPeer.colorProperty, TextBlockPeer.subtleProperty); } getToolTip(): string { return "Double click to edit"; } initializeCardElement() { if (!this.cardElement.text || this.cardElement.text == "") { this.cardElement.text = "New TextBlock"; } this.cardElement.wrap = true; } } export class RichTextBlockPeer extends TypedCardElementPeer { protected internalGetTreeItemText(): string { return this.cardElement.asString(); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, new CustomPropertySheetEntry( "*", (context: PropertySheetContext) => { let infoTextBlock = new Adaptive.TextBlock(); infoTextBlock.text = "Use the **Card Payload Editor** to edit the text of this RichTextBlock element."; infoTextBlock.wrap = true; infoTextBlock.spacing = Adaptive.Spacing.Large; infoTextBlock.separator = true; infoTextBlock.horizontalAlignment = Adaptive.HorizontalAlignment.Center; return infoTextBlock; } ) ); } initializeCardElement() { let textRun = new Adaptive.TextRun(); textRun.text = "New RichTextBlock"; this.cardElement.addInline(textRun); } } export class TableCellPeer extends ContainerPeer { canBeRemoved(): boolean { return false; } isDraggable(): boolean { return false; } } export class TableRowPeer extends TypedCardElementPeer { static readonly horizontalCellContentAlignmentProperty = new EnumPropertyEditor( Adaptive.Versions.v1_5, "horizontalCellContentAlignment", "Horizontal cell content alignment", Adaptive.HorizontalAlignment, false, true); static readonly verticalCellContentAlignmentProperty = new EnumPropertyEditor( Adaptive.Versions.v1_5, "verticalCellContentAlignment", "Vertical cell content alignment", Adaptive.VerticalAlignment, false, true); protected isContainer(): boolean { return true; } isDraggable(): boolean { return false; } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( PropertySheetCategory.StyleCategory, ContainerPeer.styleProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, TableRowPeer.horizontalCellContentAlignmentProperty, TableRowPeer.verticalCellContentAlignmentProperty); } get isTreeItemExpandedByDefault(): boolean { return false; } } class TableColumnsPropertyEditor extends PropertySheetEntry { private columnsChanged(context: PropertySheetContext, refreshPropertySheet: boolean) { context.peer.updateChildren(); context.peer.changed(refreshPropertySheet); } render(context: PropertySheetContext): Adaptive.CardElement { let result = new Adaptive.Container(); let peer = context.peer as TablePeer; if (peer.cardElement.getColumnCount() === 0) { let messageTextBlock = new Adaptive.TextBlock(); messageTextBlock.spacing = Adaptive.Spacing.Small; messageTextBlock.text = "No column has been defined"; result.addItem(messageTextBlock); } else { for (let i = 0; i < peer.cardElement.getColumnCount(); i++) { let columnDefinition = peer.cardElement.getColumnAt(i); let widthInput = new Adaptive.NumberInput(); widthInput.placeholder = "Width"; widthInput.min = 0; widthInput.defaultValue = columnDefinition.width.physicalSize; widthInput.onValueChanged = (sender) => { columnDefinition.width.physicalSize = sender.value; this.columnsChanged(context, false); }; let widthColumn = new Adaptive.Column("stretch"); widthColumn.addItem(widthInput); let unitInput = new Adaptive.ChoiceSetInput(); unitInput.placeholder = "Unit"; unitInput.choices.push( new Adaptive.Choice("Pixels", Adaptive.SizeUnit.Pixel.toString()), new Adaptive.Choice("Weight", Adaptive.SizeUnit.Weight.toString()) ); unitInput.defaultValue = columnDefinition.width.unit.toString(); unitInput.onValueChanged = (sender) => { columnDefinition.width.unit = parseInt(sender.value); this.columnsChanged(context, false); }; let unitColumn = new Adaptive.Column("stretch"); unitColumn.spacing = Adaptive.Spacing.Small; unitColumn.addItem(unitInput); let removeAction = new Adaptive.SubmitAction(); removeAction.title = "X"; removeAction.tooltip = "Remove"; removeAction.onExecute = (sender) => { peer.cardElement.removeColumn(columnDefinition); this.columnsChanged(context, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.addAction(removeAction); let removeActionColumn = new Adaptive.Column("auto"); removeActionColumn.spacing = Adaptive.Spacing.Small; removeActionColumn.addItem(actionSet); let columnSet = new Adaptive.ColumnSet(); columnSet.spacing = Adaptive.Spacing.Small; columnSet.addColumn(widthColumn); columnSet.addColumn(unitColumn); columnSet.addColumn(removeActionColumn); result.addItem(columnSet); } } let addAction = new Adaptive.SubmitAction(); addAction.title = "Add a column"; addAction.onExecute = (sender) => { peer.cardElement.addColumn(new Adaptive.TableColumnDefinition()); this.columnsChanged(context, true); }; let actionSet = new Adaptive.ActionSet(); actionSet.spacing = Adaptive.Spacing.Small; actionSet.addAction(addAction); result.addItem(actionSet); return result; } } export class TablePeer extends TypedCardElementPeer { static readonly firstRowAsHeadersProperty = new BooleanPropertyEditor( Adaptive.Versions.v1_5, "firstRowAsHeaders", "First row as headers"); static readonly cellSpacingProperty = new NumberPropertyEditor( Adaptive.Versions.v1_5, "cellSpacing", "Cell spacing (in pixels)"); static readonly showGridLinesProperty = new BooleanPropertyEditor( Adaptive.Versions.v1_5, "showGridLines", "Grid lines"); static readonly gridStyleProperty = new ContainerStylePropertyEditor(Adaptive.Versions.v1_5, "gridStyle", "Grid style"); static readonly horizontalCellContentAlignmentProperty = new EnumPropertyEditor( Adaptive.Versions.v1_5, "horizontalCellContentAlignment", "Horizontal cell content alignment", Adaptive.HorizontalAlignment, false, true); static readonly verticalCellContentAlignmentProperty = new EnumPropertyEditor( Adaptive.Versions.v1_5, "verticalCellContentAlignment", "Vertical cell content alignment", Adaptive.VerticalAlignment, false, true); static readonly columnsProperty = new TableColumnsPropertyEditor(Adaptive.Versions.v1_5); protected isContainer(): boolean { return true; } protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); commands.push( new PeerCommand( { name: "Add a row", isPromotable: false, execute: (command: PeerCommand, clickedElement: HTMLElement) => { let row = new Adaptive.TableRow(); this.cardElement.addRow(row); const newPeer = CardDesignerSurface.cardElementPeerRegistry.createPeerInstance(this.designerSurface, this, row); newPeer.insertAfterNeighbor = true; this.insertChild(newPeer); // TODO: I'm not sure if this call is necessary // this.updateChildren(); } }) ); } initializeCardElement() { super.initializeCardElement(); this.cardElement.addColumn(new Adaptive.TableColumnDefinition()); this.cardElement.addColumn(new Adaptive.TableColumnDefinition()); this.cardElement.addColumn(new Adaptive.TableColumnDefinition()); this.cardElement.addRow(new Adaptive.TableRow()); this.cardElement.addRow(new Adaptive.TableRow()); this.cardElement.addRow(new Adaptive.TableRow()); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.add( defaultCategory, TablePeer.firstRowAsHeadersProperty); propertySheet.add( "Columns", TablePeer.columnsProperty); propertySheet.add( PropertySheetCategory.StyleCategory, TablePeer.showGridLinesProperty, TablePeer.gridStyleProperty); propertySheet.add( PropertySheetCategory.LayoutCategory, TablePeer.cellSpacingProperty, TablePeer.horizontalCellContentAlignmentProperty, TablePeer.verticalCellContentAlignmentProperty); } } export class CarouselPeer extends ContainerPeer { // Question: What do we want the default value to be here? static readonly timerProperty = new CarouselTimerPropertyEditor(Adaptive.Versions.v1_6, "timer", "Timer", 5000); static readonly initialPageProperty = new NumberPropertyEditor(Adaptive.Versions.v1_6, "initialPageIndex", "Initial page", 0); static readonly orientationProperty = new EnumPropertyEditor(Adaptive.Versions.v1_6, "carouselOrientation", "Orientation", Adaptive.Orientation); static readonly heightInPixelsProperty = new NumberPropertyEditor(Adaptive.Versions.v1_6, "carouselHeight", "Height in pixels"); static readonly loopProperty = new BooleanPropertyEditor(Adaptive.Versions.v1_6, "carouselLoop", "Loop"); protected internalAddCommands(context: DesignContext, commands: Array) { super.internalAddCommands(context, commands); commands.push( new PeerCommand( { name: "Add a page", isPromotable: false, execute: (command: PeerCommand, clickedElement: HTMLElement) => { let page = new Adaptive.CarouselPage(); (this.cardElement as Adaptive.Carousel).addPage(page); this.updateChildren(); } } ) ); } initializeCardElement() { super.initializeCardElement(); (this.cardElement as Adaptive.Carousel).addPage(new Adaptive.CarouselPage()); } populatePropertySheet(propertySheet: PropertySheet, defaultCategory: string = PropertySheetCategory.DefaultCategory) { super.populatePropertySheet(propertySheet, defaultCategory); propertySheet.remove(CardElementPeer.isVisibleProperty, ContainerPeer.bleedProperty, ContainerPeer.styleProperty); propertySheet.add( defaultCategory, CarouselPeer.timerProperty, CarouselPeer.initialPageProperty, CarouselPeer.orientationProperty, CarouselPeer.heightInPixelsProperty, CarouselPeer.loopProperty); } canDrop(peer: DesignerPeer) { return false; } } export class CarouselPagePeer extends ContainerPeer { private _pageOffset: number = 0; isDraggable(): boolean { return false; } getBoundingRect(): Rect { const boundingRect = super.getBoundingRect(); const carousel = this.cardElement.parent as Adaptive.Carousel; const initialLeft = boundingRect.left; if (carousel?.carouselPageContainer) { const containerClientRect = carousel.carouselPageContainer.getBoundingClientRect(); boundingRect.left = carousel.renderedElement.offsetLeft + carousel.carouselPageContainer.offsetLeft; boundingRect.right = boundingRect.left + containerClientRect?.width; boundingRect.bottom = boundingRect.top + containerClientRect?.height; } this._pageOffset = initialLeft - boundingRect.left; return boundingRect; } peerInCorrectLocation(currentLocation: IPoint): boolean { const rect = this.getBoundingRect(); return rect.isInside(currentLocation); } isVisible(): boolean { const parentCarousel = this.parent as CarouselPeer; return this === parentCarousel?.children[(parentCarousel.cardElement as Adaptive.Carousel).currentPageIndex]; } bringCardElementIntoView(): boolean { const carouselPeer = this.parent as CarouselPeer; if (carouselPeer && carouselPeer.cardElement) { this.designerSurface.shouldPersistSelectedElement = true; const index = carouselPeer.children.indexOf(this); (carouselPeer.cardElement as Adaptive.Carousel).slideTo(index); } return true; } canDrop(peer: DesignerPeer) { return !(this.cardElement as Adaptive.CarouselPage).getForbiddenChildElements().includes(peer.getCardObject().getJsonTypeName()); } get pageOffset(): number { return this._pageOffset; } }