UNPKG

@paperbits/core

Version:
1,038 lines (818 loc) 37 kB
import * as Arrays from "@paperbits/common/arrays"; import * as Utils from "@paperbits/common/utils"; import * as Html from "@paperbits/common/html"; import * as Objects from "@paperbits/common/objects"; import { ViewManager, ViewManagerMode, IHighlightConfig, IContextCommandSet, ActiveElement, View, IContextCommand } from "@paperbits/common/ui"; import { switchToParentCommand, defaultCommandColor, deleteWidgetCommand, splitter } from "@paperbits/common/ui/commands"; import { IWidgetBinding, WidgetContext, GridItem, GridHelper } from "@paperbits/common/editing"; import { IWidgetService } from "@paperbits/common/widgets"; import { EventManager, Events } from "@paperbits/common/events"; import { Router } from "@paperbits/common/routing"; import { Bag, Keys, MouseButtons } from "@paperbits/common"; import { LocalStyles, PluginBag, StyleDefinition, VariationContract } from "@paperbits/common/styles"; import { StyleHelper } from "@paperbits/styles"; import { TextblockEditor } from "../../textblock/ko"; import { ComponentFlow } from "@paperbits/common/components"; const contentEditorElementId = "contentEditor"; export class GridEditor { private activeHighlightedGridItem: GridItem; private scrolling: boolean; private scrollTimeout: any; private pointerX: number; private pointerY: number; private activeContextualCommands: IContextCommandSet; private activeElements: Bag<ActiveElement>; private ownerDocument: Document; private selection: GridItem; private activeLayer: string; private notificationTimeout: any; constructor( private readonly viewManager: ViewManager, private readonly widgetService: IWidgetService, private readonly eventManager: EventManager, private readonly router: Router ) { this.renderContextualCommands = this.renderContextualCommands.bind(this); this.onPointerDown = this.onPointerDown.bind(this); this.initialize = this.initialize.bind(this); this.dispose = this.dispose.bind(this); this.onDelete = this.onDelete.bind(this); this.onPointerMove = this.onPointerMove.bind(this); this.onWindowScroll = this.onWindowScroll.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.activeElements = {}; } private isModelBeingEdited(gridItem: GridItem): boolean { const editorView = this.viewManager.getActiveView(); if (!editorView) { return false; } let editorComponentName: string; if (typeof gridItem.editor === "string") { editorComponentName = gridItem.editor; } else { const registration = Reflect.getMetadata("paperbits-component", gridItem.editor); editorComponentName = registration.name; } if (editorView.component.name !== editorComponentName) { return false; } return true; } private isSelected(gridItem: GridItem): boolean { const selectedElement = this.viewManager.getSelectedElement(); return selectedElement?.element === gridItem.element; } private getContextCommands(gridItem: GridItem, half: string = null): IContextCommandSet { const bindings = GridHelper.getParentWidgetBindings(gridItem.element); const providers = bindings .filter(x => !!x.provides) .map(x => x.provides) .reduce((acc, val) => acc.concat(val), []); const model = gridItem.binding.model; const binding = gridItem.binding; let parentModel; const parent = gridItem.getParent(); const parentBinding = parent?.binding; if (parentBinding) { parentModel = parentBinding.model; } const context: WidgetContext = { gridItem: gridItem, parentModel: parentModel, parentBinding: parentBinding, model: model, binding: binding, half: half, providers: providers, openHelpCenter: (articleKey: string) => { const view: View = { heading: "Help center", component: { name: "help-center", params: { articleKey: articleKey, } }, resizing: { directions: "vertically horizontally", initialWidth: 500, initialHeight: 700 }, scrolling: false }; this.viewManager.openViewAsPopup(view); }, openWidgetEditor: () => { this.viewManager.openWidgetEditor(binding); }, switchToParent: () => { const selectableParent = gridItem.getParent(null, true); if (!selectableParent) { return; } const contextualCommands = selectableParent.getContextCommands(); if (!contextualCommands) { return; } this.selectElement(selectableParent); }, switchToChild: () => { const children = gridItem.getChildren(this.activeLayer, true); if (children.length == 0) { return; } const firstChild = children[0]; this.selectElement(firstChild, true); }, deleteWidget: () => { parentModel.widgets.remove(model); parentBinding.applyChanges(); this.viewManager.clearContextualCommands(); } }; let contextualCommands: IContextCommandSet; let widgetHandler = this.widgetService.getWidgetHandler(binding); if (widgetHandler) { if (widgetHandler.getContextCommands) { try { contextualCommands = widgetHandler.getContextCommands(context); } catch (error) { console.warn(`Could not get context commands.`); } } } const defaultCommands = this.getDefaultContextCommands(context); if (!contextualCommands) { contextualCommands = defaultCommands; } const defaultCommand: IContextCommand = { controlType: "toolbox-button", callback: () => { if (!context.binding) { return; } this.viewManager.openWidgetEditor(context.binding); } }; contextualCommands.element = gridItem.element; contextualCommands.defaultCommand = defaultCommand; contextualCommands.selectCommands = contextualCommands.selectCommands !== undefined ? contextualCommands.selectCommands : defaultCommands?.selectCommands; contextualCommands.hoverCommands = contextualCommands.hoverCommands !== undefined ? contextualCommands.hoverCommands : defaultCommands?.hoverCommands; contextualCommands.deleteCommand = contextualCommands.deleteCommand !== undefined ? contextualCommands.deleteCommand : defaultCommands?.deleteCommand; return contextualCommands; } private onMouseClick(event: MouseEvent): void { const htmlElement = <HTMLElement>event.target; const htmlLinkElement = <HTMLLinkElement>htmlElement.closest("A"); if (htmlLinkElement) { event.preventDefault(); // prevent default event handling for hyperlink controls } } private onPointerMove(event: PointerEvent): void { if (this.viewManager.mode === ViewManagerMode.pause) { event.preventDefault(); event.stopPropagation(); return; } this.pointerX = event.clientX; this.pointerY = event.clientY; switch (this.viewManager.mode) { case ViewManagerMode.selecting: case ViewManagerMode.selected: case ViewManagerMode.configure: this.renderHighlightedElements(); break; case ViewManagerMode.dragging: this.renderDropHandlers(); break; } } private onPointerDown(event: PointerEvent): void { if (event.ctrlKey || event.metaKey || this.viewManager.mode === ViewManagerMode.preview) { const htmlElement = <HTMLElement>event.target; const htmlLinkElement = <HTMLLinkElement>htmlElement.closest("A"); if (!htmlLinkElement || htmlLinkElement.href.endsWith("#")) { return; } event.preventDefault(); this.router.navigateTo(htmlLinkElement.href); return; } if (this.viewManager.mode === ViewManagerMode.pause) { event.preventDefault(); event.stopPropagation(); return; } if (event.button !== MouseButtons.Main) { return; } if (this.viewManager.mode !== ViewManagerMode.selecting && this.viewManager.mode !== ViewManagerMode.selected && this.viewManager.mode !== ViewManagerMode.configure) { return; } const gridItems = this.getGridItemsUnderPointer(); if (gridItems.length === 0) { return; } const topGridItem = gridItems[0]; if (topGridItem.binding.readonly) { return; } if (topGridItem.binding.layer !== "*" && topGridItem.binding.layer !== this.activeLayer) { event.preventDefault(); event.stopPropagation(); this.eventManager.dispatchEvent("displayInactiveLayoutHint", topGridItem.binding.model); return; } if (gridItems.length === 1 && topGridItem.binding.name === "content" && topGridItem.binding.layer === this.activeLayer) { this.viewManager.clearContextualCommands(); return; } const gridItem = topGridItem; if (!gridItem) { return; } if (gridItem.editor !== TextblockEditor) { /** * Special case for text editor. All other widget element (like a hyperlink or a form submit) * should not trigger their default events. */ event.preventDefault(); } if (this.isModelBeingEdited(gridItem)) { return; } if (this.isSelected(gridItem)) { const commandSet = gridItem.getContextCommands(); if (commandSet.defaultCommand) { commandSet.defaultCommand.callback(); } } else { event.preventDefault(); // To prevent document selection. if (gridItem.element["dragSource"]) { // TODO: Maybe make part of Binding? gridItem.element["dragSource"].beginDrag(gridItem.element, this.pointerX, this.pointerY); } this.selectElement(gridItem); } } private onKeyDown(event: KeyboardEvent): void { if (document.activeElement.id !== contentEditorElementId) { return; } const selectedElement = this.viewManager.getSelectedElement(); if (!selectedElement) { return; } const gridItem = this.getGridItem(selectedElement.element); if (!gridItem) { return; } switch (event.key) { case Keys.ArrowDown: case Keys.ArrowRight: const next = gridItem.getNextSibling(); if (next) { this.selectElement(next, true); } break; case Keys.ArrowUp: case Keys.ArrowLeft: const prev = gridItem.getPrevSibling(); if (prev) { this.selectElement(prev, true); } break; case Keys.PageUp: const parent = gridItem.getParent(this.activeLayer, true); if (parent) { this.selectElement(parent, true); } break; case Keys.PageDown: const children = gridItem.getChildren(this.activeLayer, true); if (children.length > 0) { const firstChild = children[0]; this.selectElement(firstChild, true); } break; case Keys.Enter: // if (gridItem.binding.editor) { // this.viewManager.openWidgetEditor(gridItem.binding); // } break; case Keys.Escape: // On Esc we should return forcus to selected element break; default: return; // Ignore other keys } } private selectElement(item: GridItem, scrollIntoView: boolean = false): void { if (!item) { throw new Error(`Parameter "item" not specified.`); } this.eventManager.dispatchEvent(Events.NotificationRequest, item.displayName); if (this.notificationTimeout) { clearTimeout(this.notificationTimeout); } this.notificationTimeout = setTimeout(() => { this.eventManager.dispatchEvent(Events.NotificationRequest, "selected"); }, 1000); const commandSet = item?.getContextCommands(); if (!commandSet) { return; } const config: IHighlightConfig = { element: item.element, text: item.displayName, color: commandSet.color }; this.viewManager.setSelectedElement(config, commandSet); this.activeContextualCommands = commandSet; this.selection = item; document.getElementById(contentEditorElementId).focus(); if (scrollIntoView) { item.element.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } private renderDropHandlers(): void { const dragSession = this.viewManager.getDragSession(); if (!dragSession) { return; } const stack = this.getGridItemsUnderPointer(); if (stack.length === 0) { return; } const acceptingParent = stack.find(x => { if (!x.binding) { return false; } const handler = this.widgetService.getWidgetHandler(x.binding); if (handler?.canAccept && handler.canAccept(dragSession)) { return true; } return false; }); if (!acceptingParent) { delete dragSession.targetElement; delete dragSession.targetBinding; this.viewManager.setSplitter(null); return; } dragSession.targetElement = acceptingParent.element; dragSession.targetBinding = acceptingParent.binding; const siblingElement: GridItem = stack.find(x => x.element.parentElement === acceptingParent.element); if (siblingElement) { const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, siblingElement.element); const sourceElementFlow = dragSession.sourceBinding.flow || ComponentFlow.Inline; dragSession.insertIndex = acceptingParent.binding.model.widgets.indexOf(siblingElement.binding.model); const hoveredElementFlow = siblingElement.binding.flow || ComponentFlow.Inline; if (sourceElementFlow === ComponentFlow.Inline && hoveredElementFlow === ComponentFlow.Inline) { if (quadrant.horizontal === "right") { dragSession.insertIndex++; } this.viewManager.setSplitter({ element: siblingElement.element, side: quadrant.horizontal, where: "outside" }); } else { if (quadrant.vertical === "bottom") { dragSession.insertIndex++; } this.viewManager.setSplitter({ element: siblingElement.element, side: quadrant.vertical, where: "outside" }); } } else { const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, acceptingParent.element); if (acceptingParent.binding.model.widgets.length === 0) { dragSession.insertIndex = 0; this.viewManager.setSplitter({ element: acceptingParent.element, side: quadrant.vertical, where: "inside" }); return; } else { const children = Array.prototype.slice.call(acceptingParent.element.children); if (quadrant.vertical === "top") { dragSession.insertIndex = 0; const child = children[0]; this.viewManager.setSplitter({ element: child, side: "top", where: "outside" }); } else { dragSession.insertIndex = children.length; const child = children[dragSession.insertIndex]; this.viewManager.setSplitter({ element: child, side: "bottom", where: "outside" }); } } } } private onDelete(): void { if (this.viewManager.mode === ViewManagerMode.selected && this.activeContextualCommands && this.activeContextualCommands.deleteCommand) { this.activeContextualCommands.deleteCommand.callback(); } } private onWindowScroll(): void { if (this.viewManager.mode === ViewManagerMode.dragging || this.viewManager.mode === ViewManagerMode.pause || this.viewManager.mode === ViewManagerMode.configure) { return; } if (!this.scrolling) { this.viewManager.clearContextualCommands(); } this.scrolling = true; if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); } this.scrollTimeout = setTimeout(this.resetScrolling.bind(this), 100); } private resetScrolling(): void { this.scrolling = false; this.renderHighlightedElements(); if (this.selection) { // also, check for element existence. this.selectElement(this.selection, false); } } private getElementsUnderPointer(): HTMLElement[] { const elements = Utils.elementsFromPoint(this.ownerDocument, this.pointerX, this.pointerY); // Determining cut-off index to exclude from selection stack the elements under popups or dropdowns. const cutoffIndex = elements.findIndex(element => getComputedStyle(element).zIndex !== "auto"); if (cutoffIndex >= 0) { elements.splice(cutoffIndex + 1); } return elements; } private renderHighlightedElements(): void { if (this.scrolling) { return; } this.renderContextualCommands(); } private getDefaultContextCommands(context: WidgetContext): IContextCommandSet { const contextCommands: IContextCommandSet = { color: defaultCommandColor, hoverCommands: [{ controlType: "toolbox-button", color: defaultCommandColor, iconClass: "paperbits-icon paperbits-simple-add", position: context.half, tooltip: "Add widget", component: { name: "widget-selector", params: { onRequest: () => context.providers, onSelect: (newWidgetModel: any) => { let index = context.parentModel.widgets.indexOf(context.model); if (context.half === "bottom") { index++; } context.parentBinding.model.widgets.splice(index, 0, newWidgetModel); context.parentBinding.applyChanges(); this.viewManager.clearContextualCommands(); } } } }], deleteCommand: deleteWidgetCommand(context), selectCommands: [] }; if (context.binding?.editor && context.binding?.applyChanges) { contextCommands.selectCommands.push({ controlType: "toolbox-button", displayName: `Edit widget`, position: "top right", color: defaultCommandColor, callback: () => this.viewManager.openWidgetEditor(context.binding) }); contextCommands.selectCommands.push(splitter()); } contextCommands.selectCommands.push(switchToParentCommand(context)); if (!context.half) { // Not selection mode. const handler = this.widgetService.getWidgetHandler(context.binding); if (!handler?.getStyleDefinitions) { return contextCommands; } const styleDefinitions = handler.getStyleDefinitions(); if (!styleDefinitions?.components) { return contextCommands; } const styleEditorCommand = this.getStyleEditorCommand(context.gridItem.element, context.binding, styleDefinitions); if (styleEditorCommand) { contextCommands.selectCommands.push(styleEditorCommand); } } return contextCommands; } private renderContextualCommands(): void { let highlightedGridItem: GridItem; let highlightedText: string; let highlightColor: string; const tobeDeleted = Object.keys(this.activeElements); const gridItems = this.getGridItemsUnderPointer(this.activeLayer); tobeDeleted.forEach(key => { this.viewManager.removeContextualCommands(key); delete this.activeElements[key]; }); for (let i = gridItems.length - 1; i >= 0; i--) { const gridItem = gridItems[i]; const index = tobeDeleted.indexOf(gridItem.name); tobeDeleted.splice(index, 1); const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, gridItem.element); const half = quadrant.vertical; const activeElement = this.activeElements[gridItem.name]; const contextualCommandSet = gridItem.getContextCommands(half); highlightedGridItem = gridItem; highlightedText = gridItem.displayName; highlightColor = contextualCommandSet.color; if (!activeElement || gridItem.element !== activeElement.element || half !== activeElement.half) { this.viewManager.setContextualCommands(gridItem.name, contextualCommandSet); this.activeElements[gridItem.name] = { key: gridItem.name, element: gridItem.element, half: quadrant.vertical }; } } const isTopLevelWidget = highlightedGridItem?.binding.name === "content" && this.activeLayer === highlightedGridItem?.binding.layer; if (!highlightedGridItem || isTopLevelWidget) { this.activeHighlightedGridItem = null; this.viewManager.setHighlight(null); return; } if (this.activeHighlightedGridItem !== highlightedGridItem && !highlightedGridItem?.binding?.readonly) { this.activeHighlightedGridItem = highlightedGridItem; this.viewManager.setHighlight({ element: highlightedGridItem?.element, text: highlightedText, color: highlightColor }); } } private findFocusableElement(): GridItem { const element = <HTMLElement>Html.findFirst(this.ownerDocument.body, node => node.nodeName === "SECTION" && !!this.getGridItem(<HTMLElement>node)); if (!element) { return null; } return this.getGridItem(element); } private onGlobalFocusChange(event: FocusEvent): void { const target = <HTMLElement>event.target; if (target.id === contentEditorElementId) { if (this.selection) { // also, check for element existence. this.selectElement(this.selection, false); } else { const focusableElement = this.findFocusableElement(); if (focusableElement) { this.selectElement(focusableElement); } } } else { if (this.selection) { const toolbox = target.closest(".toolbox"); if (toolbox && !toolbox.classList.contains("toolbox-context")) { this.viewManager.clearSelection(); } } } } public initialize(ownerDocument: Document): void { this.activeLayer = this.viewManager.getActiveLayer(); this.ownerDocument = ownerDocument; // Firefox doesn't fire "pointermove" events by some reason this.ownerDocument.addEventListener(Events.MouseMove, this.onPointerMove, true); this.ownerDocument.addEventListener(Events.Scroll, this.onWindowScroll); this.ownerDocument.addEventListener(Events.MouseDown, this.onPointerDown, true); this.ownerDocument.addEventListener(Events.Click, this.onMouseClick, true); this.eventManager.addEventListener("onKeyDown", this.onKeyDown); this.eventManager.addEventListener("onDelete", this.onDelete); document.addEventListener(Events.Focus, (e) => this.onGlobalFocusChange(e), true); // Prevent zoom on the content ownerDocument.addEventListener("wheel", (event: WheelEvent): void => { if (event.ctrlKey) { event.preventDefault(); } const scrollTarget = <HTMLElement>event.target; scrollTarget.scrollBy(event.deltaX, event.deltaY); }, { passive: false }); } public dispose(): void { this.ownerDocument.removeEventListener(Events.MouseMove, this.onPointerMove, true); this.ownerDocument.removeEventListener(Events.Scroll, this.onWindowScroll); this.ownerDocument.removeEventListener(Events.MouseDown, this.onPointerDown, true); this.ownerDocument.removeEventListener(Events.Click, this.onMouseClick, false); this.eventManager.removeEventListener("onKeyDown", this.onKeyDown); this.eventManager.removeEventListener("onDelete", this.onDelete); } private getChildren(gridItem: GridItem, layerName?: string): GridItem[] { const childElements = Arrays.coerce<HTMLElement>(gridItem.element.querySelectorAll("*")); return childElements .map(child => this.getGridItem(child, layerName)) .filter(x => !!x && x.binding.model !== gridItem.binding.model && !x.binding.readonly); } private getParent(gridItem: GridItem, layerName?: string, excludeReadonly?: boolean): GridItem { const elements = Html.parents(gridItem.element); const parentGridItems = this.getGridItems(elements, layerName); return excludeReadonly ? parentGridItems.find(x => !x.binding.readonly) : parentGridItems.find(() => true); } private getSiblings(gridItem: GridItem): GridItem[] { const parent = this.getParent(gridItem); return this.getChildren(parent); } private getNextSibling(gridItem: GridItem): GridItem { const nextElement = <HTMLElement>gridItem.element.nextElementSibling; if (!nextElement) { return null; } return this.getGridItem(nextElement); } private getPrevSibling(gridItem: GridItem): GridItem { const previousElement = <HTMLElement>gridItem.element.previousElementSibling; if (!previousElement) { return null; } return this.getGridItem(previousElement); } private getGridItemsUnderPointer(layerName?: string): GridItem[] { const elements = this.getElementsUnderPointer().filter(x => x.tagName !== "HTML"); return this.getGridItems(elements, layerName); } private getGridItem(element: HTMLElement, requestedLayerName?: string): GridItem { const widgetBinding = GridHelper.getWidgetBinding(element); if (!widgetBinding) { return null; } const isAnotherLayer = widgetBinding.layer !== "*" && requestedLayerName && widgetBinding.layer !== requestedLayerName; if (isAnotherLayer) { return null; } const gridItem: GridItem = { name: widgetBinding.name, displayName: widgetBinding.displayName, editor: widgetBinding.editor, element: element, binding: widgetBinding, getParent: (layerName: string, excludeReadonly: boolean) => this.getParent(gridItem, layerName, excludeReadonly), getChildren: (layerName: string) => this.getChildren(gridItem, layerName), getSiblings: () => this.getSiblings(gridItem), getNextSibling: () => this.getNextSibling(gridItem), getPrevSibling: () => this.getPrevSibling(gridItem), getContextCommands: (half) => this.getContextCommands(gridItem, half), select: (scrollIntoView: boolean = false) => this.selectElement(gridItem, scrollIntoView) }; return gridItem; } private getStyleEditorCommand(element: HTMLElement, binding: IWidgetBinding<any, any>, styleDefinitions: StyleDefinition): IContextCommand { if (!styleDefinitions.components) { return null; } const componentStyleDefinitionWrapper = StyleHelper.getStyleDefinitionWrappers(styleDefinitions.components); const match = componentStyleDefinitionWrapper.find(x => element.matches(x.selector)); if (!match) { return null; } const componentStyleDefinition = match.definition; const styleKeySegments = match.key.split("/"); const styleEditorCommand: IContextCommand = { controlType: "toolbox-button", name: "edit", tooltip: "Edit styles", iconClass: "paperbits-icon paperbits-palette", position: "top right", color: "#607d8b", callback: () => { const styles: LocalStyles = binding.model?.styles; let shortKey: string; if (styleKeySegments.length > 2) { // TODO: simplify shortKey = "instance/" + styleKeySegments.slice(2).join("/"); } else { shortKey = "instance"; } let componentVariation: PluginBag; componentVariation = Objects.getObjectAt(shortKey, styles); if (!componentVariation) { componentVariation = { key: null, // key will be generated by style editor. ...componentStyleDefinition.defaults }; Objects.setValue(shortKey, styles, componentVariation); } const view: View = { heading: `${componentStyleDefinition.displayName} styles`, component: { name: "style-editor", params: { elementStyle: componentVariation, baseComponentKey: componentStyleDefinition.baseComponentKey, plugins: componentStyleDefinition.plugins, onUpdate: (): void => { binding.applyChanges(binding.model); } } }, resizing: "vertically horizontally" }; this.viewManager.openViewAsPopup(view); } }; return styleEditorCommand; } private getStylableGridItem(element: HTMLElement): GridItem { const binding = GridHelper.getClosestParentBinding(element); if (!binding) { return null; } const handler = this.widgetService.getWidgetHandler(binding); if (!handler?.getStyleDefinitions) { return null; } const styleDefinitions = handler.getStyleDefinitions(); if (!styleDefinitions?.components) { return null; } const componentStyleDefinitionWrapper = StyleHelper.getStyleDefinitionWrappers(styleDefinitions.components); const match = componentStyleDefinitionWrapper.find(x => element.matches(x.selector)); if (!match) { return null; } const selectCommands: IContextCommand[] = []; const styleEditorCommand = this.getStyleEditorCommand(element, binding, styleDefinitions); if (styleEditorCommand) { selectCommands.push(styleEditorCommand); } const componentStyleDefinition = match.definition; const gridItem: GridItem = { name: "style", displayName: componentStyleDefinition.displayName, binding: binding, element: element, editor: "style-editor", getParent: () => this.getParent(gridItem), getChildren: () => this.getChildren(gridItem), getSiblings: () => this.getSiblings(gridItem), getNextSibling: () => this.getNextSibling(gridItem), getPrevSibling: () => this.getPrevSibling(gridItem), getContextCommands: () => { const contextualCommands: IContextCommandSet = { element: element, defaultCommand: styleEditorCommand, selectCommands: [styleEditorCommand], hoverCommands: null, deleteCommand: null }; return contextualCommands; }, select: (scrollIntoView: boolean = false) => this.selectElement(gridItem, scrollIntoView) }; return gridItem; } private getGridItems(elements: HTMLElement[], layerName: string): GridItem[] { let currentwidgetBinding: IWidgetBinding<any, any>; const stackOfGridItems = []; for (const element of elements.reverse()) { const widgetGridItem = this.getGridItem(element, layerName); if (widgetGridItem && widgetGridItem.binding !== currentwidgetBinding) { stackOfGridItems.push(widgetGridItem); currentwidgetBinding = widgetGridItem.binding; continue; } const styleableGridItem = this.getStylableGridItem(element); if (styleableGridItem) { stackOfGridItems.push(styleableGridItem); } } return stackOfGridItems.reverse(); } }