From be43ed460d5f311820dc755ee36de286399890c2 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Wed, 16 Oct 2019 13:45:17 +0800 Subject: [PATCH 1/8] abstract Element class & refactor prompt node move logic --- .../extensions/visual-designer/.eslintrc.js | 3 +- .../src/constants/ElementAttributes.ts | 26 ++ .../visual-designer/src/editors/ObiEditor.tsx | 15 +- .../src/utils/cursorTracker.ts | 247 +++++++++++------- 4 files changed, 186 insertions(+), 105 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/.eslintrc.js b/Composer/packages/extensions/visual-designer/.eslintrc.js index 8234d9c966..ea421ff1e3 100644 --- a/Composer/packages/extensions/visual-designer/.eslintrc.js +++ b/Composer/packages/extensions/visual-designer/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { }, rules: { '@typescript-eslint/explicit-member-accessibility': 'off', - '@typescript-eslint/no-use-before-define': ['warn', { functions: false, classes: true }], + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }; diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts index 5695f9580d..42358e2346 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts @@ -14,3 +14,29 @@ export enum AttrNames { FocusableElement = 'data-is-focusable', SelectionIndex = 'data-selection-index', } + +export class AbstractSelectorElement { + bounds: DOMRect; + [AttrNames.SelectableElement]: string | undefined; + [AttrNames.NodeElement]: string | undefined; + [AttrNames.EdgeMenuElement]: string | undefined; + [AttrNames.FocusedId]: string | undefined; + [AttrNames.SelectedId]: string | undefined; + [AttrNames.Tab]: string | undefined; + + constructor(element: HTMLElement) { + this.bounds = element.getBoundingClientRect() as DOMRect; + + Object.keys(AttrNames).forEach(key => { + this[AttrNames[key]] = element.getAttribute(AttrNames[key]); + }); + } + + getAttribute(attrName) { + return this[attrName]; + } + + getBoundingClientRect() { + return this.bounds; + } +} diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 806fbaf985..4b8e3fbf7e 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -9,7 +9,7 @@ import { has, get } from 'lodash'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; -import { AttrNames } from '../constants/ElementAttributes'; +import { AttrNames, AbstractSelectorElement } from '../constants/ElementAttributes'; import { NodeRendererContext } from '../store/NodeRendererContext'; import { SelectionContext, SelectionContextData } from '../store/SelectionContext'; import { @@ -227,10 +227,14 @@ export const ObiEditor: FC = ({ }, }); - const querySelectableElements = (): NodeListOf => { - return document.querySelectorAll(`[${AttrNames.SelectableElement}]`); + const querySelectableElements = (): AbstractSelectorElement[] => { + const items: AbstractSelectorElement[] = []; + Array.from(document.querySelectorAll(`[${AttrNames.SelectableElement}]`)).forEach(ele => { + items.push(new AbstractSelectorElement(ele as HTMLElement)); + }); + return items; }; - const [selectableElements, setSelectableElements] = useState>(querySelectableElements()); + const [selectableElements, setSelectableElements] = useState(querySelectableElements()); const getClipboardTargetsFromContext = (): string[] => { const selectedActionIds = normalizeSelection(selectionContext.selectedIds); @@ -282,6 +286,9 @@ export const ObiEditor: FC = ({ selectedIds: [selected as string], }); focused && onFocusSteps([focused], tab); + + document.querySelector(`[${AttrNames.SelectedId}=${selected}]`) && + (document.querySelector(`[${AttrNames.SelectedId}=${selected}]`) as Element).scrollIntoView(true); break; } case KeyboardPrimaryTypes.Operation: { diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts index 7aa275bf3b..320a9bfb21 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { PromptTab } from 'shared'; import { KeyboardCommandTypes } from '../constants/KeyboardCommandTypes'; import { InitNodeSize } from '../constants/ElementSizes'; -import { AttrNames } from '../constants/ElementAttributes'; +import { AttrNames, AbstractSelectorElement } from '../constants/ElementAttributes'; enum BoundRect { Top = 'top', @@ -25,25 +26,26 @@ enum Axle { * @param assistAxle assist axle for calculating. * @param filterAttrs filtering elements */ -function localeNearestElement( - currentElement: HTMLElement, - elements: NodeListOf, +function locateNearestElement( + currentElement: AbstractSelectorElement, + elements: AbstractSelectorElement[], boundRectKey: BoundRect, assistAxle: Axle, filterAttrs?: AttrNames[] -): HTMLElement { - let neareastElement: HTMLElement = currentElement; +): AbstractSelectorElement { + let neareastElement: AbstractSelectorElement = currentElement; let minDistance = 10000; let distance = minDistance; - const elementArr = Array.from(elements).filter( - element => !filterAttrs || (filterAttrs && filterAttrs.find(key => !!element.getAttribute(key))) - ); + const elementCandidates = + filterAttrs && filterAttrs.length + ? elements.filter(el => filterAttrs.find(attr => !!el.getAttribute(attr))) + : elements; const currentElementBounds = currentElement.getBoundingClientRect(); let bounds: ClientRect; let assistMinDistance = 10000; let assistDistance; - elementArr.forEach(element => { + elementCandidates.forEach(element => { bounds = element.getBoundingClientRect(); if (boundRectKey === BoundRect.Top || boundRectKey === BoundRect.Left) { distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; @@ -73,118 +75,163 @@ function localeNearestElement( return neareastElement; } -function localeElementByTab(currentElement: HTMLElement, elements: NodeListOf, command: string) { - const elementArr = Array.from(elements); - const currentElementBounds = currentElement.getBoundingClientRect(); - let bounds: ClientRect; - let selectedElement: HTMLElement = currentElement; - let selectedElementBounds: ClientRect; - let isInvolved = false; - const judgeElementRelation = (parentBounds, childBounds) => { - return ( - parentBounds.left < childBounds.left && - parentBounds.right >= childBounds.right && - parentBounds.top < childBounds.top && - parentBounds.bottom > childBounds.bottom - ); - }; +function isParentRect(parentRect, childRect) { + return ( + parentRect.left < childRect.left && + parentRect.right >= childRect.right && + parentRect.top < childRect.top && + parentRect.bottom > childRect.bottom + ); +} + +function findSelectableChild(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { + const rect = element.getBoundingClientRect(); + return elementList.find(el => { + const candidateRect = el.getBoundingClientRect(); + return isParentRect(rect, candidateRect); + }); +} + +function findSelectableParent(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { + const rect = element.getBoundingClientRect(); + return elementList.find(el => { + const candidateRect = el.getBoundingClientRect(); + return isParentRect(candidateRect, rect); + }); +} + +function handleTabMove( + currentElement: AbstractSelectorElement, + selectableElements: AbstractSelectorElement[], + command: string +) { + let nextElement: AbstractSelectorElement; if (command === KeyboardCommandTypes.Cursor.MoveNext) { - elementArr.forEach(element => { - bounds = element.getBoundingClientRect(); - if (judgeElementRelation(currentElementBounds, bounds)) { - isInvolved = true; - selectedElement = element; - } - }); - if (!isInvolved) { - selectedElement = localeNearestElement(currentElement, elements, BoundRect.Top, Axle.X, [ + const selectableChild = findSelectableChild(currentElement, selectableElements); + if (selectableChild) { + // Tab to inner selectable element. + nextElement = selectableChild; + } else { + // Perform like presssing down arrow key. + nextElement = locateNearestElement(currentElement, selectableElements, BoundRect.Top, Axle.X, [ AttrNames.NodeElement, AttrNames.EdgeMenuElement, ]); } - } else { - elementArr.forEach(element => { - bounds = element.getBoundingClientRect(); - if (judgeElementRelation(bounds, currentElementBounds)) { - isInvolved = true; - selectedElement = element; - } - }); - if (!isInvolved) { - selectedElement = localeNearestElement(currentElement, elements, BoundRect.Bottom, Axle.X, [ + } else if (command === KeyboardCommandTypes.Cursor.MovePrevious) { + const selectableParent = findSelectableParent(currentElement, selectableElements); + if (selectableParent) { + // Tab to parent. + nextElement = selectableParent; + } else { + // Perform like pressing up arrow key. + nextElement = locateNearestElement(currentElement, selectableElements, BoundRect.Bottom, Axle.X, [ AttrNames.NodeElement, AttrNames.EdgeMenuElement, ]); - selectedElementBounds = selectedElement.getBoundingClientRect(); - elementArr.forEach(element => { - bounds = element.getBoundingClientRect(); - if (judgeElementRelation(selectedElementBounds, bounds)) { - selectedElement = element; - } - }); + // If prev element has child, tab to it before the element itself. + const selectableChildInNext = findSelectableChild(nextElement, selectableElements); + if (selectableChildInNext) { + nextElement = selectableChildInNext; + } } + } else { + // By default, stay focus on the origin element. + nextElement = currentElement; } - return selectedElement; + + return nextElement; } + +function handleArrowkeyMove( + currentElement: AbstractSelectorElement, + selectableElements: AbstractSelectorElement[], + command: string +) { + let element: AbstractSelectorElement = currentElement; + let boundRect: BoundRect = BoundRect.Bottom; + let axle: Axle = Axle.X; + let filterAttrs: AttrNames[] = []; + + if ( + currentElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && + command === KeyboardCommandTypes.Cursor.MoveUp + ) { + element = selectableElements.find( + ele => + ele.getAttribute(AttrNames.SelectedId) === + `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` + ) as AbstractSelectorElement; + } else { + switch (command) { + case KeyboardCommandTypes.Cursor.MoveDown: + boundRect = BoundRect.Top; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveUp: + boundRect = BoundRect.Bottom; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveLeft: + boundRect = BoundRect.Right; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveRight: + boundRect = BoundRect.Left; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveDown: + boundRect = BoundRect.Top; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveUp: + boundRect = BoundRect.Bottom; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveLeft: + boundRect = BoundRect.Right; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveRight: + boundRect = BoundRect.Left; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + default: + return element; + } + element = locateNearestElement(currentElement, selectableElements, boundRect, axle, filterAttrs); + } + + return element; +} + export function moveCursor( - selectedElements: NodeListOf, + selectableElements: AbstractSelectorElement[], id: string, command: string ): { [key: string]: string | undefined } { - const currentElement = Array.from(selectedElements).find( - element => element.dataset.selectedId === id || element.dataset.focusedId === id + const currentElement = selectableElements.find( + element => element.getAttribute(AttrNames.SelectedId) === id || element.getAttribute(AttrNames.FocusedId) === id ); if (!currentElement) return { selected: id, focused: undefined }; - let element: HTMLElement = currentElement; + let element: AbstractSelectorElement = currentElement; switch (command) { - case KeyboardCommandTypes.Cursor.MoveDown: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Top, Axle.X, [AttrNames.NodeElement]); - break; - case KeyboardCommandTypes.Cursor.MoveUp: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Bottom, Axle.X, [ - AttrNames.NodeElement, - ]); - break; - case KeyboardCommandTypes.Cursor.MoveLeft: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Right, Axle.Y, [ - AttrNames.NodeElement, - ]); - break; - case KeyboardCommandTypes.Cursor.MoveRight: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Left, Axle.Y, [AttrNames.NodeElement]); - break; - case KeyboardCommandTypes.Cursor.ShortMoveDown: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Top, Axle.X, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - break; - case KeyboardCommandTypes.Cursor.ShortMoveUp: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Bottom, Axle.X, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - break; - case KeyboardCommandTypes.Cursor.ShortMoveLeft: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Right, Axle.Y, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - break; - case KeyboardCommandTypes.Cursor.ShortMoveRight: - element = localeNearestElement(currentElement, selectedElements, BoundRect.Left, Axle.Y, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - break; case KeyboardCommandTypes.Cursor.MovePrevious: - element = localeElementByTab(currentElement, selectedElements, KeyboardCommandTypes.Cursor.MovePrevious); - break; case KeyboardCommandTypes.Cursor.MoveNext: - element = localeElementByTab(currentElement, selectedElements, KeyboardCommandTypes.Cursor.MoveNext); + element = handleTabMove(currentElement, selectableElements, command); + break; + default: + element = handleArrowkeyMove(currentElement, selectableElements, command); break; } - element.scrollIntoView(true); return { selected: element.getAttribute(AttrNames.SelectedId) || id, From 78481417a317ad03d03d3890936beb5513e6499a Mon Sep 17 00:00:00 2001 From: Long Jun Date: Wed, 16 Oct 2019 20:06:02 +0800 Subject: [PATCH 2/8] refactor cursor move --- .../visual-designer/src/editors/ObiEditor.tsx | 4 +- .../src/utils/cursorTracker.ts | 212 ++++++++++++------ 2 files changed, 145 insertions(+), 71 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 4b8e3fbf7e..78845e30f4 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -287,8 +287,8 @@ export const ObiEditor: FC = ({ }); focused && onFocusSteps([focused], tab); - document.querySelector(`[${AttrNames.SelectedId}=${selected}]`) && - (document.querySelector(`[${AttrNames.SelectedId}=${selected}]`) as Element).scrollIntoView(true); + document.querySelector(`[${AttrNames.SelectedId}="${selected}"]`) && + (document.querySelector(`[${AttrNames.SelectedId}="${selected}"]`) as Element).scrollIntoView(true); break; } case KeyboardPrimaryTypes.Operation: { diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts index 320a9bfb21..f6a6d5e4be 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts @@ -34,6 +34,7 @@ function locateNearestElement( filterAttrs?: AttrNames[] ): AbstractSelectorElement { let neareastElement: AbstractSelectorElement = currentElement; + const neareastElements: AbstractSelectorElement[] = []; let minDistance = 10000; let distance = minDistance; const elementCandidates = @@ -45,33 +46,117 @@ function locateNearestElement( let assistMinDistance = 10000; let assistDistance; - elementCandidates.forEach(element => { - bounds = element.getBoundingClientRect(); - if (boundRectKey === BoundRect.Top || boundRectKey === BoundRect.Left) { - distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; - } else { - distance = currentElementBounds[boundRectKey] - bounds[boundRectKey]; + if (assistAxle === Axle.X) { + // move up & down + // prompt element with exception tab: + // moveUp to bot_ask tab + // moveDown: stay focus on original element + if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS) { + if (boundRectKey === BoundRect.Bottom) { + return elementCandidates.find( + ele => + ele.getAttribute(AttrNames.SelectedId) === + `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` + ) as AbstractSelectorElement; + } else { + return currentElement; + } } + elementCandidates.forEach(element => { + bounds = element.getBoundingClientRect(); - if (assistAxle === Axle.X) { assistDistance = Math.abs( currentElementBounds.left + currentElementBounds.width / 2 - (bounds.left + bounds.width / 2) ); + if (boundRectKey === BoundRect.Top) { + distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; + } else { + distance = currentElementBounds[boundRectKey] - bounds[boundRectKey]; + } if (assistDistance < InitNodeSize.width / 2 && distance > 0 && distance < minDistance) { neareastElement = element; minDistance = distance; } - } else { + }); + + // If neareastElement is prompt node with exception tab and original node is not a prompt node, then stay focus on original element + if ( + neareastElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && + !currentElement.getAttribute(AttrNames.Tab) + ) { + return currentElement; + } + } else { + // move left & right + let secondMinDistance = 1000; + let secondAssistMinDistance = 1000; + elementCandidates.forEach(element => { + bounds = element.getBoundingClientRect(); + + if (boundRectKey === BoundRect.Left) { + distance = + bounds[boundRectKey] + + bounds.width / 2 - + (currentElementBounds[boundRectKey] + currentElementBounds.width / 2); + } else { + distance = + currentElementBounds[boundRectKey] - + currentElementBounds.width / 2 - + (bounds[boundRectKey] - bounds.width / 2); + } assistDistance = Math.abs( currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) ); - if (distance > 0 && distance <= minDistance && assistMinDistance >= assistDistance) { - neareastElement = element; - minDistance = distance; - assistMinDistance = assistDistance; + // find three element who is closer to the current element + if (distance > 0) { + if (distance <= minDistance && assistDistance <= assistMinDistance) { + neareastElements[0] = element; + minDistance = distance; + assistMinDistance = assistDistance; + } else { + if (distance <= secondMinDistance) { + neareastElements[1] = element; + secondMinDistance = distance; + } + if (assistDistance <= secondAssistMinDistance) { + neareastElements[2] = element; + secondAssistMinDistance = assistDistance; + } + } } - } - }); + }); + + const currentElementIdArrs = currentElement.getAttribute(AttrNames.SelectedId).split('.'); + let maxSamePath = 0; + let samePathCount = 0; + let samePath: string = currentElementIdArrs[0]; + let eleSelectedId = ''; + + neareastElements.forEach(ele => { + samePath = currentElementIdArrs[0]; + samePathCount = 0; + eleSelectedId = ele.getAttribute(AttrNames.SelectedId); + for (let i = 1; i < currentElementIdArrs.length; i++) { + if (eleSelectedId.includes(samePath)) { + samePath += `.${currentElementIdArrs[i]}`; + samePathCount++; + } + } + + // If the element's selectedId includes the original element's or its selectedId has the most overlap with the original element and selectedId's length is not more than the original element's, it is the neareast element + // Else stay focus on the original element + if ( + samePathCount > maxSamePath && + !(ele.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && !currentElement.getAttribute(AttrNames.Tab)) && + (eleSelectedId.split('.').length <= currentElementIdArrs.length || + eleSelectedId.includes(currentElementIdArrs.join('.'))) + ) { + neareastElement = ele; + maxSamePath = samePathCount; + } + }); + } + return neareastElement; } @@ -153,62 +238,51 @@ function handleArrowkeyMove( let axle: Axle = Axle.X; let filterAttrs: AttrNames[] = []; - if ( - currentElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && - command === KeyboardCommandTypes.Cursor.MoveUp - ) { - element = selectableElements.find( - ele => - ele.getAttribute(AttrNames.SelectedId) === - `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` - ) as AbstractSelectorElement; - } else { - switch (command) { - case KeyboardCommandTypes.Cursor.MoveDown: - boundRect = BoundRect.Top; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveUp: - boundRect = BoundRect.Bottom; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveLeft: - boundRect = BoundRect.Right; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveRight: - boundRect = BoundRect.Left; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveDown: - boundRect = BoundRect.Top; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveUp: - boundRect = BoundRect.Bottom; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveLeft: - boundRect = BoundRect.Right; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveRight: - boundRect = BoundRect.Left; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - default: - return element; - } - element = locateNearestElement(currentElement, selectableElements, boundRect, axle, filterAttrs); + switch (command) { + case KeyboardCommandTypes.Cursor.MoveDown: + boundRect = BoundRect.Top; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveUp: + boundRect = BoundRect.Bottom; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveLeft: + boundRect = BoundRect.Right; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.MoveRight: + boundRect = BoundRect.Left; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveDown: + boundRect = BoundRect.Top; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveUp: + boundRect = BoundRect.Bottom; + axle = Axle.X; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveLeft: + boundRect = BoundRect.Right; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + case KeyboardCommandTypes.Cursor.ShortMoveRight: + boundRect = BoundRect.Left; + axle = Axle.Y; + filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; + break; + default: + return element; } + element = locateNearestElement(currentElement, selectableElements, boundRect, axle, filterAttrs); return element; } From 8f4d24ec1dfa36a6d5dd18972412849d294c2b33 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Wed, 16 Oct 2019 20:44:25 +0800 Subject: [PATCH 3/8] fix switch case --- .../src/utils/cursorTracker.ts | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts index f6a6d5e4be..1a3484a8b1 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts @@ -88,6 +88,31 @@ function locateNearestElement( } } else { // move left & right + elementCandidates.forEach(element => { + bounds = element.getBoundingClientRect(); + + if (boundRectKey === BoundRect.Left) { + distance = + bounds[boundRectKey] + + bounds.width / 2 - + (currentElementBounds[boundRectKey] + currentElementBounds.width / 2); + } else { + distance = + currentElementBounds[boundRectKey] - + currentElementBounds.width / 2 - + (bounds[boundRectKey] - bounds.width / 2); + } + assistDistance = Math.abs( + currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) + ); + // find three element who is closer to the current element + if (distance > 0 && distance <= minDistance && assistDistance <= assistMinDistance) { + neareastElements[0] = element; + minDistance = distance; + assistMinDistance = assistDistance; + } + }); + let secondMinDistance = 1000; let secondAssistMinDistance = 1000; elementCandidates.forEach(element => { @@ -109,23 +134,16 @@ function locateNearestElement( ); // find three element who is closer to the current element if (distance > 0) { - if (distance <= minDistance && assistDistance <= assistMinDistance) { - neareastElements[0] = element; - minDistance = distance; - assistMinDistance = assistDistance; - } else { - if (distance <= secondMinDistance) { - neareastElements[1] = element; - secondMinDistance = distance; - } - if (assistDistance <= secondAssistMinDistance) { - neareastElements[2] = element; - secondAssistMinDistance = assistDistance; - } + if (distance <= secondMinDistance && distance > minDistance) { + neareastElements[1] = element; + secondMinDistance = distance; + } + if (assistDistance <= secondAssistMinDistance && assistDistance > assistMinDistance) { + neareastElements[2] = element; + secondAssistMinDistance = assistDistance; } } }); - const currentElementIdArrs = currentElement.getAttribute(AttrNames.SelectedId).split('.'); let maxSamePath = 0; let samePathCount = 0; @@ -143,12 +161,19 @@ function locateNearestElement( } } + // calculate switchCondition default branch length + let currentElementActionLength = currentElementIdArrs.length; + currentElementIdArrs.forEach(action => { + if (action.includes('default')) { + currentElementActionLength++; + } + }); // If the element's selectedId includes the original element's or its selectedId has the most overlap with the original element and selectedId's length is not more than the original element's, it is the neareast element // Else stay focus on the original element if ( samePathCount > maxSamePath && !(ele.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && !currentElement.getAttribute(AttrNames.Tab)) && - (eleSelectedId.split('.').length <= currentElementIdArrs.length || + (eleSelectedId.split('.').length <= currentElementActionLength || eleSelectedId.includes(currentElementIdArrs.join('.'))) ) { neareastElement = ele; From 4ada07f2d9d754e7c7715f6b5087b6170aeb71bb Mon Sep 17 00:00:00 2001 From: Long Jun Date: Wed, 6 Nov 2019 16:36:00 +0800 Subject: [PATCH 4/8] add selectorElement class --- .../demo/src/stories/VisualEditorDemo.js | 2 +- .../src/constants/ElementAttributes.ts | 26 ------------- .../visual-designer/src/editors/ObiEditor.tsx | 12 ++---- .../src/models/SelectorElement.ts | 29 ++++++++++++++ .../src/utils/cursorTracker.ts | 39 ++++++++++++------- 5 files changed, 57 insertions(+), 51 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js index 232b5ab896..17c7c5c6ec 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualEditorDemo.js @@ -83,7 +83,7 @@ export class VisualEditorDemo extends Component { data={obiJson} dialogId={selectedFile} focusedEvent={focusedEvent} - focusedSteps={focusedSteps} + focusedActions={focusedSteps} focusedTab={focusedTab} clipboardActions={clipboardActions} shellApi={{ diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts index 42358e2346..5695f9580d 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementAttributes.ts @@ -14,29 +14,3 @@ export enum AttrNames { FocusableElement = 'data-is-focusable', SelectionIndex = 'data-selection-index', } - -export class AbstractSelectorElement { - bounds: DOMRect; - [AttrNames.SelectableElement]: string | undefined; - [AttrNames.NodeElement]: string | undefined; - [AttrNames.EdgeMenuElement]: string | undefined; - [AttrNames.FocusedId]: string | undefined; - [AttrNames.SelectedId]: string | undefined; - [AttrNames.Tab]: string | undefined; - - constructor(element: HTMLElement) { - this.bounds = element.getBoundingClientRect() as DOMRect; - - Object.keys(AttrNames).forEach(key => { - this[AttrNames[key]] = element.getAttribute(AttrNames[key]); - }); - } - - getAttribute(attrName) { - return this[attrName]; - } - - getBoundingClientRect() { - return this.bounds; - } -} diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 78845e30f4..9a99b457b6 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -9,7 +9,8 @@ import { has, get } from 'lodash'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; -import { AttrNames, AbstractSelectorElement } from '../constants/ElementAttributes'; +import { AttrNames } from '../constants/ElementAttributes'; +import { AbstractSelectorElement } from '../models/SelectorElement'; import { NodeRendererContext } from '../store/NodeRendererContext'; import { SelectionContext, SelectionContextData } from '../store/SelectionContext'; import { @@ -21,7 +22,7 @@ import { pasteNodes, deleteNodes, } from '../utils/jsonTracker'; -import { moveCursor } from '../utils/cursorTracker'; +import { moveCursor, querySelectableElements } from '../utils/cursorTracker'; import { NodeIndexGenerator } from '../utils/NodeIndexGetter'; import { normalizeSelection } from '../utils/normalizeSelection'; import { KeyboardZone } from '../components/lib/KeyboardZone'; @@ -227,13 +228,6 @@ export const ObiEditor: FC = ({ }, }); - const querySelectableElements = (): AbstractSelectorElement[] => { - const items: AbstractSelectorElement[] = []; - Array.from(document.querySelectorAll(`[${AttrNames.SelectableElement}]`)).forEach(ele => { - items.push(new AbstractSelectorElement(ele as HTMLElement)); - }); - return items; - }; const [selectableElements, setSelectableElements] = useState(querySelectableElements()); const getClipboardTargetsFromContext = (): string[] => { diff --git a/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts b/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts new file mode 100644 index 0000000000..c02d68a72c --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { AttrNames } from '../constants/ElementAttributes'; +export class AbstractSelectorElement { + bounds: DOMRect; + [AttrNames.SelectableElement]: string | undefined; + [AttrNames.NodeElement]: string | undefined; + [AttrNames.EdgeMenuElement]: string | undefined; + [AttrNames.FocusedId]: string | undefined; + [AttrNames.SelectedId]: string | undefined; + [AttrNames.Tab]: string | undefined; + + constructor(element: HTMLElement) { + this.bounds = element.getBoundingClientRect() as DOMRect; + + Object.keys(AttrNames).forEach(key => { + this[AttrNames[key]] = element.getAttribute(AttrNames[key]); + }); + } + + getAttribute(attrName) { + return this[attrName]; + } + + getBoundingClientRect() { + return this.bounds; + } +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts index 1a3484a8b1..5f353ca758 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { PromptTab } from 'shared'; +import { PromptTab } from '@bfc/shared'; import { KeyboardCommandTypes } from '../constants/KeyboardCommandTypes'; import { InitNodeSize } from '../constants/ElementSizes'; -import { AttrNames, AbstractSelectorElement } from '../constants/ElementAttributes'; +import { AttrNames } from '../constants/ElementAttributes'; +import { AbstractSelectorElement } from '../models/SelectorElement'; enum BoundRect { Top = 'top', @@ -48,10 +49,10 @@ function locateNearestElement( if (assistAxle === Axle.X) { // move up & down - // prompt element with exception tab: + // prompt element with OTHER tab: // moveUp to bot_ask tab // moveDown: stay focus on original element - if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS) { + if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER) { if (boundRectKey === BoundRect.Bottom) { return elementCandidates.find( ele => @@ -81,7 +82,7 @@ function locateNearestElement( // If neareastElement is prompt node with exception tab and original node is not a prompt node, then stay focus on original element if ( - neareastElement.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && + neareastElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER && !currentElement.getAttribute(AttrNames.Tab) ) { return currentElement; @@ -172,7 +173,7 @@ function locateNearestElement( // Else stay focus on the original element if ( samePathCount > maxSamePath && - !(ele.getAttribute(AttrNames.Tab) === PromptTab.EXCEPTIONS && !currentElement.getAttribute(AttrNames.Tab)) && + !(ele.getAttribute(AttrNames.Tab) === PromptTab.OTHER && !currentElement.getAttribute(AttrNames.Tab)) && (eleSelectedId.split('.').length <= currentElementActionLength || eleSelectedId.includes(currentElementIdArrs.join('.'))) ) { @@ -194,9 +195,9 @@ function isParentRect(parentRect, childRect) { ); } -function findSelectableChild(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { +function findSelectableChildren(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { const rect = element.getBoundingClientRect(); - return elementList.find(el => { + return elementList.filter(el => { const candidateRect = el.getBoundingClientRect(); return isParentRect(rect, candidateRect); }); @@ -217,10 +218,10 @@ function handleTabMove( ) { let nextElement: AbstractSelectorElement; if (command === KeyboardCommandTypes.Cursor.MoveNext) { - const selectableChild = findSelectableChild(currentElement, selectableElements); - if (selectableChild) { + const selectableChildren = findSelectableChildren(currentElement, selectableElements); + if (selectableChildren.length > 0) { // Tab to inner selectable element. - nextElement = selectableChild; + nextElement = selectableChildren[0]; } else { // Perform like presssing down arrow key. nextElement = locateNearestElement(currentElement, selectableElements, BoundRect.Top, Axle.X, [ @@ -239,10 +240,10 @@ function handleTabMove( AttrNames.NodeElement, AttrNames.EdgeMenuElement, ]); - // If prev element has child, tab to it before the element itself. - const selectableChildInNext = findSelectableChild(nextElement, selectableElements); - if (selectableChildInNext) { - nextElement = selectableChildInNext; + // If prev element has children, tab to the last child before the element itself. + const selectableChildInNext = findSelectableChildren(nextElement, selectableElements); + if (selectableChildInNext.length > 0) { + nextElement = selectableChildInNext[selectableChildInNext.length - 1]; } } } else { @@ -338,3 +339,11 @@ export function moveCursor( tab: element.getAttribute(AttrNames.Tab) || '', }; } + +export function querySelectableElements(): AbstractSelectorElement[] { + const items: AbstractSelectorElement[] = []; + Array.from(document.querySelectorAll(`[${AttrNames.SelectableElement}]`)).forEach(ele => { + items.push(new AbstractSelectorElement(ele as HTMLElement)); + }); + return items; +} From b4c17e9eacf0df68fee9c7f8dc7095a13857b759 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Thu, 7 Nov 2019 20:16:53 +0800 Subject: [PATCH 5/8] adjust algorithm --- .../src/utils/cursorTracker.ts | 309 +++++++++++------- 1 file changed, 199 insertions(+), 110 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts index 5f353ca758..61d701be0b 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { PromptTab } from '@bfc/shared'; +import { element } from 'prop-types'; +import { InitNodeSize, LoopEdgeMarginLeft, IconBrickSize, ElementInterval } from '../constants/ElementSizes'; import { KeyboardCommandTypes } from '../constants/KeyboardCommandTypes'; -import { InitNodeSize } from '../constants/ElementSizes'; import { AttrNames } from '../constants/ElementAttributes'; import { AbstractSelectorElement } from '../models/SelectorElement'; @@ -19,79 +20,36 @@ enum Axle { Y, } -/** - * - * @param currentElement current element - * @param elements all elements have AttrNames.SelectableElements attribute - * @param boundRectKey key to calculate shortest distance - * @param assistAxle assist axle for calculating. - * @param filterAttrs filtering elements - */ -function locateNearestElement( +interface ElementVector { + distance: number; + assistDistance: number; + selectedId: string; +} + +// calculate vector between element and currentElement +function calculateElementVector( currentElement: AbstractSelectorElement, elements: AbstractSelectorElement[], boundRectKey: BoundRect, - assistAxle: Axle, - filterAttrs?: AttrNames[] -): AbstractSelectorElement { - let neareastElement: AbstractSelectorElement = currentElement; - const neareastElements: AbstractSelectorElement[] = []; - let minDistance = 10000; - let distance = minDistance; - const elementCandidates = - filterAttrs && filterAttrs.length - ? elements.filter(el => filterAttrs.find(attr => !!el.getAttribute(attr))) - : elements; + assistAxle: Axle +): ElementVector[] { const currentElementBounds = currentElement.getBoundingClientRect(); - let bounds: ClientRect; - let assistMinDistance = 10000; - let assistDistance; - - if (assistAxle === Axle.X) { - // move up & down - // prompt element with OTHER tab: - // moveUp to bot_ask tab - // moveDown: stay focus on original element - if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER) { - if (boundRectKey === BoundRect.Bottom) { - return elementCandidates.find( - ele => - ele.getAttribute(AttrNames.SelectedId) === - `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` - ) as AbstractSelectorElement; - } else { - return currentElement; - } - } - elementCandidates.forEach(element => { - bounds = element.getBoundingClientRect(); + const elementVectors: ElementVector[] = []; + elements.forEach(element => { + const bounds: ClientRect = element.getBoundingClientRect(); + let distance: number; + let assistDistance: number; - assistDistance = Math.abs( - currentElementBounds.left + currentElementBounds.width / 2 - (bounds.left + bounds.width / 2) - ); + if (assistAxle === Axle.X) { if (boundRectKey === BoundRect.Top) { distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; } else { distance = currentElementBounds[boundRectKey] - bounds[boundRectKey]; } - if (assistDistance < InitNodeSize.width / 2 && distance > 0 && distance < minDistance) { - neareastElement = element; - minDistance = distance; - } - }); - - // If neareastElement is prompt node with exception tab and original node is not a prompt node, then stay focus on original element - if ( - neareastElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER && - !currentElement.getAttribute(AttrNames.Tab) - ) { - return currentElement; - } - } else { - // move left & right - elementCandidates.forEach(element => { - bounds = element.getBoundingClientRect(); - + assistDistance = Math.abs( + currentElementBounds.left + currentElementBounds.width / 2 - (bounds.left + bounds.width / 2) + ); + } else { if (boundRectKey === BoundRect.Left) { distance = bounds[boundRectKey] + @@ -106,55 +64,126 @@ function locateNearestElement( assistDistance = Math.abs( currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) ); - // find three element who is closer to the current element - if (distance > 0 && distance <= minDistance && assistDistance <= assistMinDistance) { - neareastElements[0] = element; - minDistance = distance; - assistMinDistance = assistDistance; - } + } + elementVectors.push({ + distance, + assistDistance, + selectedId: element.getAttribute(AttrNames.SelectedId), }); + }); + return elementVectors; +} - let secondMinDistance = 1000; - let secondAssistMinDistance = 1000; - elementCandidates.forEach(element => { - bounds = element.getBoundingClientRect(); +function locateCandidateElementsByVenctor(elements: ElementVector[], assistAxle: Axle): ElementVector[] { + const candidates: ElementVector[] = [ + { + distance: 1000, + assistDistance: 100000, + selectedId: '', + }, + { + distance: 1000, + assistDistance: 1000, + selectedId: '', + }, + { + distance: 1000, + assistDistance: 1000, + selectedId: '', + }, + ]; + if (assistAxle === Axle.X) { + elements.forEach(element => { + if ( + element.assistDistance < InitNodeSize.width / 2 && + element.distance > 0 && + element.distance < candidates[0].distance + ) { + candidates[0] = element; + } + }); + } else { + // x is the length of the botAsk node and the exception node on x-axis + const x = (IconBrickSize.width + InitNodeSize.width) / 2 + LoopEdgeMarginLeft; - if (boundRectKey === BoundRect.Left) { - distance = - bounds[boundRectKey] + - bounds.width / 2 - - (currentElementBounds[boundRectKey] + currentElementBounds.width / 2); - } else { - distance = - currentElementBounds[boundRectKey] - - currentElementBounds.width / 2 - - (bounds[boundRectKey] - bounds.width / 2); + elements.forEach(element => { + // find three element who is closer to the current element + if ( + element.distance > 0 && + (element.distance > x && + element.distance / element.assistDistance > candidates[0].distance / candidates[0].assistDistance) + ) { + candidates[0] = element; } - assistDistance = Math.abs( - currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) - ); + }); + elements.forEach(element => { // find three element who is closer to the current element - if (distance > 0) { - if (distance <= secondMinDistance && distance > minDistance) { - neareastElements[1] = element; - secondMinDistance = distance; + if (element.distance > 0) { + if (element.distance <= candidates[1].distance && element.distance <= candidates[0].distance) { + candidates[1] = element; } - if (assistDistance <= secondAssistMinDistance && assistDistance > assistMinDistance) { - neareastElements[2] = element; - secondAssistMinDistance = assistDistance; + if ( + element.assistDistance <= candidates[2].assistDistance && + element.assistDistance <= candidates[0].assistDistance + ) { + candidates[2] = element; } } }); + } + return candidates; +} + +function getActionLength(element: AbstractSelectorElement): number { + const arrs = element.getAttribute(AttrNames.SelectedId).split('.'); + let length = arrs.length; + + arrs.forEach(action => { + if (action.includes('default')) { + length++; + } + }); + return length; +} + +function locateNearestElementBySchema( + candidateElements: AbstractSelectorElement[], + currentElement: AbstractSelectorElement, + assistAxle: Axle, + boundRectKey: BoundRect +): AbstractSelectorElement { + let neareastElement: AbstractSelectorElement = currentElement; + + if (assistAxle === Axle.X) { + // prompt element with OTHER tab: + // moveUp to bot_ask tab + // moveDown: stay focus on original element + if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER) { + if (boundRectKey === BoundRect.Bottom) { + neareastElement = candidateElements.find( + ele => + ele.getAttribute(AttrNames.SelectedId) === + `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` + ) as AbstractSelectorElement; + } else { + neareastElement = currentElement; + } + } else { + neareastElement = candidateElements[0]; + } + } else { const currentElementIdArrs = currentElement.getAttribute(AttrNames.SelectedId).split('.'); let maxSamePath = 0; let samePathCount = 0; let samePath: string = currentElementIdArrs[0]; let eleSelectedId = ''; + let eleActionLength = 0; - neareastElements.forEach(ele => { + candidateElements.forEach(ele => { samePath = currentElementIdArrs[0]; samePathCount = 0; eleSelectedId = ele.getAttribute(AttrNames.SelectedId); + eleActionLength = getActionLength(ele); for (let i = 1; i < currentElementIdArrs.length; i++) { if (eleSelectedId.includes(samePath)) { samePath += `.${currentElementIdArrs[i]}`; @@ -162,26 +191,84 @@ function locateNearestElement( } } - // calculate switchCondition default branch length - let currentElementActionLength = currentElementIdArrs.length; - currentElementIdArrs.forEach(action => { - if (action.includes('default')) { - currentElementActionLength++; - } - }); // If the element's selectedId includes the original element's or its selectedId has the most overlap with the original element and selectedId's length is not more than the original element's, it is the neareast element // Else stay focus on the original element - if ( - samePathCount > maxSamePath && - !(ele.getAttribute(AttrNames.Tab) === PromptTab.OTHER && !currentElement.getAttribute(AttrNames.Tab)) && - (eleSelectedId.split('.').length <= currentElementActionLength || - eleSelectedId.includes(currentElementIdArrs.join('.'))) - ) { - neareastElement = ele; - maxSamePath = samePathCount; + if (!(ele.getAttribute(AttrNames.Tab) === PromptTab.OTHER && !currentElement.getAttribute(AttrNames.Tab))) { + if (samePathCount > maxSamePath) { + neareastElement = ele; + maxSamePath = samePathCount; + } else if (samePathCount === maxSamePath) { + if ( + eleActionLength < getActionLength(neareastElement) || + (eleActionLength > getActionLength(neareastElement) && eleActionLength <= getActionLength(currentElement)) + ) { + neareastElement = ele; + maxSamePath = samePathCount; + } else { + const eleActionArr = eleSelectedId.split('.'); + const nearElementActionArr = neareastElement.getAttribute(AttrNames.SelectedId).split('.'); + const currentElementDifferentActionIndex = Number( + currentElementIdArrs[maxSamePath].substring( + currentElementIdArrs[maxSamePath].indexOf('[') + 1, + currentElementIdArrs[maxSamePath].indexOf(']') + ) + ); + const eleDifferentActionIndex = Number( + eleActionArr[maxSamePath].substring( + eleActionArr[maxSamePath].indexOf('[') + 1, + eleActionArr[maxSamePath].indexOf(']') + ) + ); + const neareastElementDifferentActionIndex = Number( + nearElementActionArr[maxSamePath].substring( + nearElementActionArr[maxSamePath].indexOf('[') + 1, + nearElementActionArr[maxSamePath].indexOf(']') + ) + ); + + if ( + Math.abs(currentElementDifferentActionIndex - eleDifferentActionIndex) < + Math.abs(currentElementDifferentActionIndex - neareastElementDifferentActionIndex) + ) { + neareastElement = ele; + maxSamePath = samePathCount; + } + } + } } }); } + return neareastElement; +} +/** + * + * @param currentElement current element + * @param elements all elements have AttrNames.SelectableElements attribute + * @param boundRectKey key to calculate shortest distance + * @param assistAxle assist axle for calculating. + * @param filterAttrs filtering elements + */ +function locateNearestElement( + currentElement: AbstractSelectorElement, + elements: AbstractSelectorElement[], + boundRectKey: BoundRect, + assistAxle: Axle, + filterAttrs?: AttrNames[] +): AbstractSelectorElement { + // Get elements that meet the filter criteria + const elementArr = elements.filter( + element => !filterAttrs || (filterAttrs && filterAttrs.find(key => !!element.getAttribute(key))) + ); + + // Calculate element's vector and choose candidate elements by comparing elements' position + const elementVectors = calculateElementVector(currentElement, elementArr, boundRectKey, assistAxle); + const candidateElementVenctors = locateCandidateElementsByVenctor(elementVectors, assistAxle); + const candidateElements = elementArr.filter(element => + candidateElementVenctors.find(venctor => venctor.selectedId === element.getAttribute(AttrNames.SelectedId)) + ); + + // Choose the neareastElement by schema + const neareastElement = locateNearestElementBySchema(candidateElements, currentElement, assistAxle, boundRectKey); return neareastElement; } @@ -323,6 +410,8 @@ export function moveCursor( ); if (!currentElement) return { selected: id, focused: undefined }; let element: AbstractSelectorElement = currentElement; + + // tab move or arrow move switch (command) { case KeyboardCommandTypes.Cursor.MovePrevious: case KeyboardCommandTypes.Cursor.MoveNext: From c906892f5e749d3f1c32f823ff33ccc8cdafa036 Mon Sep 17 00:00:00 2001 From: Long Jun Date: Thu, 14 Nov 2019 17:40:04 +0800 Subject: [PATCH 6/8] refine cursorTracker module --- .../visual-designer/src/editors/ObiEditor.tsx | 5 +- .../src/models/SelectorElement.ts | 29 -- .../src/utils/cursorTracker.ts | 438 ------------------ .../src/utils/cursorTracker/arrowMove.ts | 57 +++ .../calculate/calculateBySchema.ts | 211 +++++++++ .../calculate/calculateByVector.ts | 102 ++++ .../utils/cursorTracker/calculate/index.ts | 31 ++ .../src/utils/cursorTracker/index.ts | 46 ++ .../src/utils/cursorTracker/tabMove.ts | 64 +++ .../src/utils/cursorTracker/type.ts | 55 +++ 10 files changed, 568 insertions(+), 470 deletions(-) delete mode 100644 Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts delete mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/index.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts create mode 100644 Composer/packages/extensions/visual-designer/src/utils/cursorTracker/type.ts diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index 9a99b457b6..b2d3e8a1b1 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -10,7 +10,6 @@ import { has, get } from 'lodash'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; import { AttrNames } from '../constants/ElementAttributes'; -import { AbstractSelectorElement } from '../models/SelectorElement'; import { NodeRendererContext } from '../store/NodeRendererContext'; import { SelectionContext, SelectionContextData } from '../store/SelectionContext'; import { @@ -22,7 +21,7 @@ import { pasteNodes, deleteNodes, } from '../utils/jsonTracker'; -import { moveCursor, querySelectableElements } from '../utils/cursorTracker'; +import { moveCursor, querySelectableElements, SelectorElement } from '../utils/cursorTracker/index'; import { NodeIndexGenerator } from '../utils/NodeIndexGetter'; import { normalizeSelection } from '../utils/normalizeSelection'; import { KeyboardZone } from '../components/lib/KeyboardZone'; @@ -228,7 +227,7 @@ export const ObiEditor: FC = ({ }, }); - const [selectableElements, setSelectableElements] = useState(querySelectableElements()); + const [selectableElements, setSelectableElements] = useState(querySelectableElements()); const getClipboardTargetsFromContext = (): string[] => { const selectedActionIds = normalizeSelection(selectionContext.selectedIds); diff --git a/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts b/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts deleted file mode 100644 index c02d68a72c..0000000000 --- a/Composer/packages/extensions/visual-designer/src/models/SelectorElement.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { AttrNames } from '../constants/ElementAttributes'; -export class AbstractSelectorElement { - bounds: DOMRect; - [AttrNames.SelectableElement]: string | undefined; - [AttrNames.NodeElement]: string | undefined; - [AttrNames.EdgeMenuElement]: string | undefined; - [AttrNames.FocusedId]: string | undefined; - [AttrNames.SelectedId]: string | undefined; - [AttrNames.Tab]: string | undefined; - - constructor(element: HTMLElement) { - this.bounds = element.getBoundingClientRect() as DOMRect; - - Object.keys(AttrNames).forEach(key => { - this[AttrNames[key]] = element.getAttribute(AttrNames[key]); - }); - } - - getAttribute(attrName) { - return this[attrName]; - } - - getBoundingClientRect() { - return this.bounds; - } -} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts deleted file mode 100644 index 61d701be0b..0000000000 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker.ts +++ /dev/null @@ -1,438 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -import { PromptTab } from '@bfc/shared'; -import { element } from 'prop-types'; - -import { InitNodeSize, LoopEdgeMarginLeft, IconBrickSize, ElementInterval } from '../constants/ElementSizes'; -import { KeyboardCommandTypes } from '../constants/KeyboardCommandTypes'; -import { AttrNames } from '../constants/ElementAttributes'; -import { AbstractSelectorElement } from '../models/SelectorElement'; - -enum BoundRect { - Top = 'top', - Bottom = 'bottom', - Left = 'left', - Right = 'right', -} - -enum Axle { - X, - Y, -} - -interface ElementVector { - distance: number; - assistDistance: number; - selectedId: string; -} - -// calculate vector between element and currentElement -function calculateElementVector( - currentElement: AbstractSelectorElement, - elements: AbstractSelectorElement[], - boundRectKey: BoundRect, - assistAxle: Axle -): ElementVector[] { - const currentElementBounds = currentElement.getBoundingClientRect(); - const elementVectors: ElementVector[] = []; - elements.forEach(element => { - const bounds: ClientRect = element.getBoundingClientRect(); - let distance: number; - let assistDistance: number; - - if (assistAxle === Axle.X) { - if (boundRectKey === BoundRect.Top) { - distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; - } else { - distance = currentElementBounds[boundRectKey] - bounds[boundRectKey]; - } - assistDistance = Math.abs( - currentElementBounds.left + currentElementBounds.width / 2 - (bounds.left + bounds.width / 2) - ); - } else { - if (boundRectKey === BoundRect.Left) { - distance = - bounds[boundRectKey] + - bounds.width / 2 - - (currentElementBounds[boundRectKey] + currentElementBounds.width / 2); - } else { - distance = - currentElementBounds[boundRectKey] - - currentElementBounds.width / 2 - - (bounds[boundRectKey] - bounds.width / 2); - } - assistDistance = Math.abs( - currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) - ); - } - elementVectors.push({ - distance, - assistDistance, - selectedId: element.getAttribute(AttrNames.SelectedId), - }); - }); - return elementVectors; -} - -function locateCandidateElementsByVenctor(elements: ElementVector[], assistAxle: Axle): ElementVector[] { - const candidates: ElementVector[] = [ - { - distance: 1000, - assistDistance: 100000, - selectedId: '', - }, - { - distance: 1000, - assistDistance: 1000, - selectedId: '', - }, - { - distance: 1000, - assistDistance: 1000, - selectedId: '', - }, - ]; - if (assistAxle === Axle.X) { - elements.forEach(element => { - if ( - element.assistDistance < InitNodeSize.width / 2 && - element.distance > 0 && - element.distance < candidates[0].distance - ) { - candidates[0] = element; - } - }); - } else { - // x is the length of the botAsk node and the exception node on x-axis - const x = (IconBrickSize.width + InitNodeSize.width) / 2 + LoopEdgeMarginLeft; - - elements.forEach(element => { - // find three element who is closer to the current element - if ( - element.distance > 0 && - (element.distance > x && - element.distance / element.assistDistance > candidates[0].distance / candidates[0].assistDistance) - ) { - candidates[0] = element; - } - }); - elements.forEach(element => { - // find three element who is closer to the current element - if (element.distance > 0) { - if (element.distance <= candidates[1].distance && element.distance <= candidates[0].distance) { - candidates[1] = element; - } - if ( - element.assistDistance <= candidates[2].assistDistance && - element.assistDistance <= candidates[0].assistDistance - ) { - candidates[2] = element; - } - } - }); - } - return candidates; -} - -function getActionLength(element: AbstractSelectorElement): number { - const arrs = element.getAttribute(AttrNames.SelectedId).split('.'); - let length = arrs.length; - - arrs.forEach(action => { - if (action.includes('default')) { - length++; - } - }); - return length; -} - -function locateNearestElementBySchema( - candidateElements: AbstractSelectorElement[], - currentElement: AbstractSelectorElement, - assistAxle: Axle, - boundRectKey: BoundRect -): AbstractSelectorElement { - let neareastElement: AbstractSelectorElement = currentElement; - - if (assistAxle === Axle.X) { - // prompt element with OTHER tab: - // moveUp to bot_ask tab - // moveDown: stay focus on original element - if (currentElement.getAttribute(AttrNames.Tab) === PromptTab.OTHER) { - if (boundRectKey === BoundRect.Bottom) { - neareastElement = candidateElements.find( - ele => - ele.getAttribute(AttrNames.SelectedId) === - `${currentElement.getAttribute(AttrNames.FocusedId)}${PromptTab.BOT_ASKS}` - ) as AbstractSelectorElement; - } else { - neareastElement = currentElement; - } - } else { - neareastElement = candidateElements[0]; - } - } else { - const currentElementIdArrs = currentElement.getAttribute(AttrNames.SelectedId).split('.'); - let maxSamePath = 0; - let samePathCount = 0; - let samePath: string = currentElementIdArrs[0]; - let eleSelectedId = ''; - let eleActionLength = 0; - - candidateElements.forEach(ele => { - samePath = currentElementIdArrs[0]; - samePathCount = 0; - eleSelectedId = ele.getAttribute(AttrNames.SelectedId); - eleActionLength = getActionLength(ele); - for (let i = 1; i < currentElementIdArrs.length; i++) { - if (eleSelectedId.includes(samePath)) { - samePath += `.${currentElementIdArrs[i]}`; - samePathCount++; - } - } - - // If the element's selectedId includes the original element's or its selectedId has the most overlap with the original element and selectedId's length is not more than the original element's, it is the neareast element - // Else stay focus on the original element - if (!(ele.getAttribute(AttrNames.Tab) === PromptTab.OTHER && !currentElement.getAttribute(AttrNames.Tab))) { - if (samePathCount > maxSamePath) { - neareastElement = ele; - maxSamePath = samePathCount; - } else if (samePathCount === maxSamePath) { - if ( - eleActionLength < getActionLength(neareastElement) || - (eleActionLength > getActionLength(neareastElement) && eleActionLength <= getActionLength(currentElement)) - ) { - neareastElement = ele; - maxSamePath = samePathCount; - } else { - const eleActionArr = eleSelectedId.split('.'); - const nearElementActionArr = neareastElement.getAttribute(AttrNames.SelectedId).split('.'); - const currentElementDifferentActionIndex = Number( - currentElementIdArrs[maxSamePath].substring( - currentElementIdArrs[maxSamePath].indexOf('[') + 1, - currentElementIdArrs[maxSamePath].indexOf(']') - ) - ); - const eleDifferentActionIndex = Number( - eleActionArr[maxSamePath].substring( - eleActionArr[maxSamePath].indexOf('[') + 1, - eleActionArr[maxSamePath].indexOf(']') - ) - ); - const neareastElementDifferentActionIndex = Number( - nearElementActionArr[maxSamePath].substring( - nearElementActionArr[maxSamePath].indexOf('[') + 1, - nearElementActionArr[maxSamePath].indexOf(']') - ) - ); - - if ( - Math.abs(currentElementDifferentActionIndex - eleDifferentActionIndex) < - Math.abs(currentElementDifferentActionIndex - neareastElementDifferentActionIndex) - ) { - neareastElement = ele; - maxSamePath = samePathCount; - } - } - } - } - }); - } - return neareastElement; -} -/** - * - * @param currentElement current element - * @param elements all elements have AttrNames.SelectableElements attribute - * @param boundRectKey key to calculate shortest distance - * @param assistAxle assist axle for calculating. - * @param filterAttrs filtering elements - */ -function locateNearestElement( - currentElement: AbstractSelectorElement, - elements: AbstractSelectorElement[], - boundRectKey: BoundRect, - assistAxle: Axle, - filterAttrs?: AttrNames[] -): AbstractSelectorElement { - // Get elements that meet the filter criteria - const elementArr = elements.filter( - element => !filterAttrs || (filterAttrs && filterAttrs.find(key => !!element.getAttribute(key))) - ); - - // Calculate element's vector and choose candidate elements by comparing elements' position - const elementVectors = calculateElementVector(currentElement, elementArr, boundRectKey, assistAxle); - const candidateElementVenctors = locateCandidateElementsByVenctor(elementVectors, assistAxle); - const candidateElements = elementArr.filter(element => - candidateElementVenctors.find(venctor => venctor.selectedId === element.getAttribute(AttrNames.SelectedId)) - ); - - // Choose the neareastElement by schema - const neareastElement = locateNearestElementBySchema(candidateElements, currentElement, assistAxle, boundRectKey); - - return neareastElement; -} - -function isParentRect(parentRect, childRect) { - return ( - parentRect.left < childRect.left && - parentRect.right >= childRect.right && - parentRect.top < childRect.top && - parentRect.bottom > childRect.bottom - ); -} - -function findSelectableChildren(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { - const rect = element.getBoundingClientRect(); - return elementList.filter(el => { - const candidateRect = el.getBoundingClientRect(); - return isParentRect(rect, candidateRect); - }); -} - -function findSelectableParent(element: AbstractSelectorElement, elementList: AbstractSelectorElement[]) { - const rect = element.getBoundingClientRect(); - return elementList.find(el => { - const candidateRect = el.getBoundingClientRect(); - return isParentRect(candidateRect, rect); - }); -} - -function handleTabMove( - currentElement: AbstractSelectorElement, - selectableElements: AbstractSelectorElement[], - command: string -) { - let nextElement: AbstractSelectorElement; - if (command === KeyboardCommandTypes.Cursor.MoveNext) { - const selectableChildren = findSelectableChildren(currentElement, selectableElements); - if (selectableChildren.length > 0) { - // Tab to inner selectable element. - nextElement = selectableChildren[0]; - } else { - // Perform like presssing down arrow key. - nextElement = locateNearestElement(currentElement, selectableElements, BoundRect.Top, Axle.X, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - } - } else if (command === KeyboardCommandTypes.Cursor.MovePrevious) { - const selectableParent = findSelectableParent(currentElement, selectableElements); - if (selectableParent) { - // Tab to parent. - nextElement = selectableParent; - } else { - // Perform like pressing up arrow key. - nextElement = locateNearestElement(currentElement, selectableElements, BoundRect.Bottom, Axle.X, [ - AttrNames.NodeElement, - AttrNames.EdgeMenuElement, - ]); - // If prev element has children, tab to the last child before the element itself. - const selectableChildInNext = findSelectableChildren(nextElement, selectableElements); - if (selectableChildInNext.length > 0) { - nextElement = selectableChildInNext[selectableChildInNext.length - 1]; - } - } - } else { - // By default, stay focus on the origin element. - nextElement = currentElement; - } - - return nextElement; -} - -function handleArrowkeyMove( - currentElement: AbstractSelectorElement, - selectableElements: AbstractSelectorElement[], - command: string -) { - let element: AbstractSelectorElement = currentElement; - let boundRect: BoundRect = BoundRect.Bottom; - let axle: Axle = Axle.X; - let filterAttrs: AttrNames[] = []; - - switch (command) { - case KeyboardCommandTypes.Cursor.MoveDown: - boundRect = BoundRect.Top; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveUp: - boundRect = BoundRect.Bottom; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveLeft: - boundRect = BoundRect.Right; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.MoveRight: - boundRect = BoundRect.Left; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveDown: - boundRect = BoundRect.Top; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveUp: - boundRect = BoundRect.Bottom; - axle = Axle.X; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveLeft: - boundRect = BoundRect.Right; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - case KeyboardCommandTypes.Cursor.ShortMoveRight: - boundRect = BoundRect.Left; - axle = Axle.Y; - filterAttrs = [AttrNames.NodeElement, AttrNames.EdgeMenuElement]; - break; - default: - return element; - } - element = locateNearestElement(currentElement, selectableElements, boundRect, axle, filterAttrs); - - return element; -} - -export function moveCursor( - selectableElements: AbstractSelectorElement[], - id: string, - command: string -): { [key: string]: string | undefined } { - const currentElement = selectableElements.find( - element => element.getAttribute(AttrNames.SelectedId) === id || element.getAttribute(AttrNames.FocusedId) === id - ); - if (!currentElement) return { selected: id, focused: undefined }; - let element: AbstractSelectorElement = currentElement; - - // tab move or arrow move - switch (command) { - case KeyboardCommandTypes.Cursor.MovePrevious: - case KeyboardCommandTypes.Cursor.MoveNext: - element = handleTabMove(currentElement, selectableElements, command); - break; - default: - element = handleArrowkeyMove(currentElement, selectableElements, command); - break; - } - - return { - selected: element.getAttribute(AttrNames.SelectedId) || id, - focused: element.getAttribute(AttrNames.FocusedId) || undefined, - tab: element.getAttribute(AttrNames.Tab) || '', - }; -} - -export function querySelectableElements(): AbstractSelectorElement[] { - const items: AbstractSelectorElement[] = []; - Array.from(document.querySelectorAll(`[${AttrNames.SelectableElement}]`)).forEach(ele => { - items.push(new AbstractSelectorElement(ele as HTMLElement)); - }); - return items; -} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts new file mode 100644 index 0000000000..3495f15131 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { KeyboardCommandTypes } from '../../constants/KeyboardCommandTypes'; + +import { locateNearestElement } from './calculate/index'; +import { SelectorElement, Direction } from './type'; + +export function handleArrowkeyMove( + currentElement: SelectorElement, + selectableElements: SelectorElement[], + command: string +) { + let element: SelectorElement = currentElement; + let direction: Direction; + let filterAttrs: string[] = []; + + switch (command) { + case KeyboardCommandTypes.Cursor.MoveDown: + direction = Direction.Down; + filterAttrs = ['isNode']; + break; + case KeyboardCommandTypes.Cursor.MoveUp: + direction = Direction.Up; + filterAttrs = ['isNode']; + break; + case KeyboardCommandTypes.Cursor.MoveLeft: + direction = Direction.Left; + filterAttrs = ['isNode']; + break; + case KeyboardCommandTypes.Cursor.MoveRight: + direction = Direction.Right; + filterAttrs = ['isNode']; + break; + case KeyboardCommandTypes.Cursor.ShortMoveDown: + direction = Direction.Down; + filterAttrs = ['isNode', 'isEdgeMenu']; + break; + case KeyboardCommandTypes.Cursor.ShortMoveUp: + direction = Direction.Up; + filterAttrs = ['isNode', 'isEdgeMenu']; + break; + case KeyboardCommandTypes.Cursor.ShortMoveLeft: + direction = Direction.Left; + filterAttrs = ['isNode', 'isEdgeMenu']; + break; + case KeyboardCommandTypes.Cursor.ShortMoveRight: + direction = Direction.Right; + filterAttrs = ['isNode', 'isEdgeMenu']; + break; + default: + return element; + } + element = locateNearestElement(currentElement, selectableElements, direction, filterAttrs); + + return element; +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts new file mode 100644 index 0000000000..d300fff3f1 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { PromptTab } from '@bfc/shared'; +import { directive } from '@babel/types'; + +import { SelectorElement, Direction } from '../type'; +function parseSelector(path: string): null | string[] { + if (!path) return null; + + const selectors = path.split('.'); + if (selectors.length === 0) { + return null; + } + + if (selectors[0] === '$' || selectors[0] === '') { + selectors.shift(); + } + + const normalizedSelectors = selectors.reduce( + (result, selector) => { + // e.g. actions[0] + const parseResult = /(\w+)\[(\-\d+|\d+)\]/.exec(selector); + + if (parseResult) { + const [, objSelector, arraySelector] = parseResult; + const arrayIndex = parseInt(arraySelector); + result.push(objSelector, arrayIndex); + } else { + result.push(selector); + } + + return result; + }, + [] as any[] + ); + + return normalizedSelectors; +} +function transformDefaultBranch(path) { + return path.replace(new RegExp('default', 'g'), 'default[-1].actions'); +} +export function filterPromptElementsBySchema( + currentElement: SelectorElement, + elements: SelectorElement[], + direction: Direction +): SelectorElement[] { + let candidateElements: SelectorElement[] = elements; + + switch (direction) { + case Direction.Up: + if (currentElement.tab === PromptTab.OTHER || currentElement.tab === PromptTab.USER_INPUT) { + candidateElements = elements.filter( + ele => ele.selectedId === `${currentElement.focusedId}${PromptTab.BOT_ASKS}` + ); + } + break; + case Direction.Down: + if (currentElement.tab === PromptTab.USER_INPUT) { + candidateElements = elements.filter(ele => ele.tab !== PromptTab.OTHER); + } + break; + case Direction.Right: + if (!currentElement.tab) { + candidateElements = elements.filter(ele => ele.tab !== PromptTab.OTHER); + } else if (currentElement.tab === PromptTab.BOT_ASKS || currentElement.tab === PromptTab.USER_INPUT) { + candidateElements = elements.filter(ele => ele.selectedId === `${currentElement.focusedId}${PromptTab.OTHER}`); + } + break; + default: + candidateElements = elements; + } + return candidateElements; +} + +function handleNextMoveFilter(currentElement: SelectorElement, elements: SelectorElement[]): SelectorElement[] { + const currentElementSelectors = parseSelector(transformDefaultBranch(currentElement.selectedId)) as string[]; + return elements.filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + const condition1 = + eleSelectors.length === currentElementSelectors.length && + Number(eleSelectors[eleSelectors.length - 1]) - 1 <= + Number(currentElementSelectors[currentElementSelectors.length - 1]); + const condition2 = + eleSelectors.length > currentElementSelectors.length && + ele.selectedId.includes(currentElement.selectedId) && + Number(eleSelectors[eleSelectors.length - 1]) === 0; + const condition3 = + eleSelectors.length < currentElementSelectors.length && + Number(eleSelectors[eleSelectors.length - 1]) - 1 === Number(currentElementSelectors[eleSelectors.length - 1]); + return condition1 || condition2 || condition3; + }); +} + +function handlePrevMoveFilter(currentElement: SelectorElement, elements: SelectorElement[]): SelectorElement[] { + const currentElementSelectors = parseSelector(transformDefaultBranch(currentElement.selectedId)) as string[]; + return elements.filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + const condition1 = + eleSelectors.length === currentElementSelectors.length && + Number(eleSelectors[eleSelectors.length - 1]) + 1 >= + Number(currentElementSelectors[currentElementSelectors.length - 1]); + const condition2 = + eleSelectors.length > currentElementSelectors.length && + Number(eleSelectors[currentElementSelectors.length - 1]) - 1 === + Number(currentElementSelectors[currentElementSelectors.length - 1]); + const condition3 = + eleSelectors.length < currentElementSelectors.length && + currentElement.selectedId.includes(ele.selectedId) && + Number(currentElementSelectors[currentElementSelectors.length - 1]) === 0; + return condition1 || condition2 || condition3; + }); +} + +function handleSwitchCasePrevMoveFilter( + currentElement: SelectorElement, + elements: SelectorElement[] +): SelectorElement[] { + const currentElementSelectors = parseSelector(transformDefaultBranch(currentElement.selectedId)) as string[]; + let candidateElements = elements; + let swicthPosition = -1; + swicthPosition = currentElementSelectors.lastIndexOf('cases'); + const samePath = currentElementSelectors.slice(0, swicthPosition).join('.'); + const sortedElement = elements + .filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + return ( + eleSelectors.slice(0, swicthPosition).join('.') === samePath && + Number(eleSelectors[swicthPosition + 1]) <= Number(currentElementSelectors[swicthPosition + 1]) + ); + }) + .sort((ele1, ele2) => { + const eleSelectors1 = parseSelector(transformDefaultBranch(ele1.selectedId)) as string[]; + const eleSelectors2 = parseSelector(transformDefaultBranch(ele2.selectedId)) as string[]; + return Number(eleSelectors2[swicthPosition + 1]) - Number(eleSelectors1[swicthPosition + 1]); + }); + const minSwitchCasesElement = parseSelector(transformDefaultBranch(sortedElement[0].selectedId)) as string[]; + const minSwitchCasesIndex = Number(minSwitchCasesElement[swicthPosition + 1]); + candidateElements = elements.filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + return Number(eleSelectors[swicthPosition + 1]) === minSwitchCasesIndex; + }); + return candidateElements; +} + +function handleSwitchCaseNextMoveFilter(currentElement: SelectorElement, elements: SelectorElement[]) { + const currentElementSelectors = parseSelector(transformDefaultBranch(currentElement.selectedId)) as string[]; + let candidateElements = elements; + let swicthPosition = -1; + if (currentElementSelectors.lastIndexOf('default') > currentElementSelectors.lastIndexOf('cases')) { + swicthPosition = currentElementSelectors.lastIndexOf('default'); + } else { + swicthPosition = currentElementSelectors.lastIndexOf('cases'); + } + const samePath = currentElementSelectors.slice(0, swicthPosition).join('.'); + const sortedElement = elements + .filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + return ( + eleSelectors.slice(0, swicthPosition).join('.') === samePath && + Number(eleSelectors[swicthPosition + 1]) >= Number(currentElementSelectors[swicthPosition + 1]) + ); + }) + .sort((ele1, ele2) => { + const eleSelectors1 = parseSelector(transformDefaultBranch(ele1.selectedId)) as string[]; + const eleSelectors2 = parseSelector(transformDefaultBranch(ele2.selectedId)) as string[]; + return Number(eleSelectors1[swicthPosition + 1]) - Number(eleSelectors2[swicthPosition + 1]); + }); + const minSwitchCasesElement = parseSelector(transformDefaultBranch(sortedElement[0].selectedId)) as string[]; + const minSwitchCasesIndex = Number(minSwitchCasesElement[swicthPosition + 1]); + candidateElements = elements.filter(ele => { + const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; + return Number(eleSelectors[swicthPosition + 1]) === minSwitchCasesIndex; + }); + return candidateElements; +} + +export function filterElementBySchema( + currentElement: SelectorElement, + elements: SelectorElement[], + direction: Direction +) { + const currentElementSelectors = parseSelector(transformDefaultBranch(currentElement.selectedId)) as string[]; + let candidateElements = elements; + switch (direction) { + case Direction.Up: { + candidateElements = handlePrevMoveFilter(currentElement, elements); + break; + } + case Direction.Down: { + candidateElements = handleNextMoveFilter(currentElement, elements); + break; + } + case Direction.Left: { + if (currentElementSelectors.lastIndexOf('cases') > -1) { + candidateElements = handleSwitchCasePrevMoveFilter(currentElement, elements); + } else { + candidateElements = handlePrevMoveFilter(currentElement, elements); + } + break; + } + case Direction.Right: { + if (currentElementSelectors.lastIndexOf('default') > -1 || currentElementSelectors.lastIndexOf('cases') > -1) { + candidateElements = handleSwitchCaseNextMoveFilter(currentElement, elements); + } else { + candidateElements = handleNextMoveFilter(currentElement, elements); + } + break; + } + } + return candidateElements; +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts new file mode 100644 index 0000000000..72e46bba85 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { SelectorElement, Axle, BoundRect, Direction } from '../type'; +interface ElementVector { + distance: number; + assistDistance: number; + selectedId: string; +} + +function transformDirectionToVectorAttrs(direction: Direction): Record { + switch (direction) { + case Direction.Up: + return { assistAxle: Axle.X, boundRectKey: BoundRect.Bottom }; + case Direction.Down: + return { assistAxle: Axle.X, boundRectKey: BoundRect.Top }; + case Direction.Left: + return { assistAxle: Axle.Y, boundRectKey: BoundRect.Right }; + case Direction.Right: + return { assistAxle: Axle.Y, boundRectKey: BoundRect.Left }; + } +} + +function transformVectorToElement(vectors: ElementVector[], elements: SelectorElement[]): SelectorElement[] { + const results: SelectorElement[] = []; + vectors.forEach(vector => + results.push(elements.find(element => vector.selectedId === element.selectedId) as SelectorElement) + ); + return results; +} +// calculate vector between element and currentElement +function calculateElementVector( + currentElement: SelectorElement, + elements: SelectorElement[], + boundRectKey: BoundRect, + assistAxle: Axle +): ElementVector[] { + const currentElementBounds = currentElement.bounds; + const elementVectors: ElementVector[] = []; + elements.forEach(element => { + const bounds = element.bounds; + let distance: number; + let assistDistance: number; + + if (assistAxle === Axle.X) { + if (boundRectKey === BoundRect.Top) { + distance = bounds[boundRectKey] - currentElementBounds[boundRectKey]; + } else { + distance = currentElementBounds[boundRectKey] - bounds[boundRectKey]; + } + assistDistance = Math.abs( + currentElementBounds.left + currentElementBounds.width / 2 - (bounds.left + bounds.width / 2) + ); + } else { + if (boundRectKey === BoundRect.Left) { + distance = + bounds[boundRectKey] + + bounds.width / 2 - + (currentElementBounds[boundRectKey] + currentElementBounds.width / 2); + } else { + distance = + currentElementBounds[boundRectKey] - + currentElementBounds.width / 2 - + (bounds[boundRectKey] - bounds.width / 2); + } + assistDistance = Math.abs( + currentElementBounds.top + currentElementBounds.height / 2 - (bounds.top + bounds.height / 2) + ); + } + elementVectors.push({ + distance, + assistDistance, + selectedId: element.selectedId as string, + }); + }); + return elementVectors; +} + +export function sortElementsByVector( + currentElement: SelectorElement, + elements: SelectorElement[], + direction: Direction +): SelectorElement[] { + const { assistAxle, boundRectKey } = transformDirectionToVectorAttrs(direction); + const elementVectors = calculateElementVector(currentElement, elements, boundRectKey, assistAxle); + const candidates = elementVectors.sort( + (ele1, ele2) => ele2.distance / ele2.assistDistance - ele1.distance / ele1.assistDistance + ); + + return transformVectorToElement(candidates, elements); +} + +export function filterElementsByVector( + currentElement: SelectorElement, + elements: SelectorElement[], + direction: Direction +): SelectorElement[] { + const { assistAxle, boundRectKey } = transformDirectionToVectorAttrs(direction); + const elementVectors = calculateElementVector(currentElement, elements, boundRectKey, assistAxle); + const candidates: ElementVector[] = elementVectors.filter(ele => ele.distance > 0); + + return transformVectorToElement(candidates, elements); +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts new file mode 100644 index 0000000000..8c44a5d741 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SelectorElement, Direction } from '../type'; + +import { filterElementsByVector, sortElementsByVector } from './calculateByVector'; +import { filterPromptElementsBySchema, filterElementBySchema } from './calculateBySchema'; +/** + * + * @param currentElement current element + * @param elements all elements have AttrNames.SelectableElements attribute + * @param boundRectKey key to calculate shortest distance + * @param assistAxle assist axle for calculating. + * @param filterAttrs filtering elements + */ +export function locateNearestElement( + currentElement: SelectorElement, + elements: SelectorElement[], + direction: Direction, + filterAttrs?: string[] +): SelectorElement { + // Get elements that meet the filter criteria + let elementArr = elements.filter(element => !filterAttrs || (filterAttrs && filterAttrs.find(key => !!element[key]))); + + elementArr = filterPromptElementsBySchema(currentElement, elementArr, direction); + elementArr = filterElementsByVector(currentElement, elementArr, direction); + elementArr = sortElementsByVector(currentElement, elementArr, direction); + elementArr = filterElementBySchema(currentElement, elementArr, direction); + + return elementArr.length > 0 ? elementArr[0] : currentElement; +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/index.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/index.ts new file mode 100644 index 0000000000..f213bb5a44 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/index.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { AttrNames } from '../../constants/ElementAttributes'; +import { KeyboardCommandTypes } from '../../constants/KeyboardCommandTypes'; + +import { SelectorElement } from './type'; +import { handleArrowkeyMove } from './arrowMove'; +import { handleTabMove } from './tabMove'; + +export function moveCursor( + selectableElements: SelectorElement[], + id: string, + command: string +): { [key: string]: string | undefined } { + const currentElement = selectableElements.find(element => element.selectedId === id || element.focusedId === id); + if (!currentElement) return { selected: id, focused: undefined }; + let element: SelectorElement = currentElement; + + // tab move or arrow move + switch (command) { + case KeyboardCommandTypes.Cursor.MovePrevious: + case KeyboardCommandTypes.Cursor.MoveNext: + element = handleTabMove(currentElement, selectableElements, command); + break; + default: + element = handleArrowkeyMove(currentElement, selectableElements, command); + break; + } + + return { + selected: element.selectedId || id, + focused: element.focusedId || undefined, + tab: element.tab || '', + }; +} + +export function querySelectableElements(): SelectorElement[] { + const items: SelectorElement[] = []; + Array.from(document.querySelectorAll(`[${AttrNames.SelectableElement}]`)).forEach(ele => { + items.push(new SelectorElement(ele as HTMLElement)); + }); + return items; +} + +export { SelectorElement }; diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts new file mode 100644 index 0000000000..cd91778afc --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { KeyboardCommandTypes } from '../../constants/KeyboardCommandTypes'; + +import { SelectorElement, Direction } from './type'; +import { locateNearestElement } from './calculate/index'; + +function isParentRect(parentRect, childRect) { + return ( + parentRect.left < childRect.left && + parentRect.right >= childRect.right && + parentRect.top < childRect.top && + parentRect.bottom > childRect.bottom + ); +} + +function findSelectableChildren(element: SelectorElement, elementList: SelectorElement[]) { + const rect = element.bounds; + return elementList.filter(el => { + const candidateRect = el.bounds; + return isParentRect(rect, candidateRect); + }); +} + +function findSelectableParent(element: SelectorElement, elementList: SelectorElement[]) { + const rect = element.bounds; + return elementList.find(el => { + const candidateRect = el.bounds; + return isParentRect(candidateRect, rect); + }); +} + +export function handleTabMove(currentElement: SelectorElement, selectableElements: SelectorElement[], command: string) { + let nextElement: SelectorElement; + if (command === KeyboardCommandTypes.Cursor.MoveNext) { + const selectableChildren = findSelectableChildren(currentElement, selectableElements); + if (selectableChildren.length > 0) { + // Tab to inner selectable element. + nextElement = selectableChildren[0]; + } else { + // Perform like presssing down arrow key. + nextElement = locateNearestElement(currentElement, selectableElements, Direction.Down, ['isNode', 'isEdgeMenu']); + } + } else if (command === KeyboardCommandTypes.Cursor.MovePrevious) { + const selectableParent = findSelectableParent(currentElement, selectableElements); + if (selectableParent) { + // Tab to parent. + nextElement = selectableParent; + } else { + // Perform like pressing up arrow key. + nextElement = locateNearestElement(currentElement, selectableElements, Direction.Up, ['isNode', 'isEdgeMenu']); + // If prev element has children, tab to the last child before the element itself. + const selectableChildInNext = findSelectableChildren(nextElement, selectableElements); + if (selectableChildInNext.length > 0) { + nextElement = selectableChildInNext[selectableChildInNext.length - 1]; + } + } + } else { + // By default, stay focus on the origin element. + nextElement = currentElement; + } + + return nextElement; +} diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/type.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/type.ts new file mode 100644 index 0000000000..8f45f690e4 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/type.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { AttrNames } from '../../constants/ElementAttributes'; + +export class SelectorElement { + bounds: Record; + isSelectable: string | null; + isNode: string | null; + isEdgeMenu: string | null; + focusedId: string | null; + selectedId: string; + tab: string | null; + + constructor(element: HTMLElement) { + this.isSelectable = element.getAttribute(AttrNames.SelectableElement); + this.isNode = element.getAttribute(AttrNames.NodeElement); + this.isEdgeMenu = element.getAttribute(AttrNames.EdgeMenuElement); + this.focusedId = element.getAttribute(AttrNames.FocusedId); + this.selectedId = element.getAttribute(AttrNames.SelectedId) as string; + this.tab = element.getAttribute(AttrNames.Tab); + + const elementBounds = element.getBoundingClientRect(); + this.bounds = { + width: elementBounds.width, + height: elementBounds.height, + left: elementBounds.left, + right: elementBounds.right, + top: elementBounds.top, + bottom: elementBounds.bottom, + }; + } + + hasAttribute(attrName) { + return this[attrName]; + } +} + +export enum Direction { + Up, + Down, + Left, + Right, +} + +export enum BoundRect { + Top = 'top', + Bottom = 'bottom', + Left = 'left', + Right = 'right', +} + +export enum Axle { + X, + Y, +} From 2b6bdf641dfd2ce931bf82f46d4ac7a8b7307a8c Mon Sep 17 00:00:00 2001 From: Long Jun Date: Fri, 15 Nov 2019 12:30:36 +0800 Subject: [PATCH 7/8] lint --- .../src/utils/cursorTracker/calculate/calculateBySchema.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts index d300fff3f1..6400b0b121 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { PromptTab } from '@bfc/shared'; -import { directive } from '@babel/types'; import { SelectorElement, Direction } from '../type'; function parseSelector(path: string): null | string[] { @@ -19,7 +18,7 @@ function parseSelector(path: string): null | string[] { const normalizedSelectors = selectors.reduce( (result, selector) => { // e.g. actions[0] - const parseResult = /(\w+)\[(\-\d+|\d+)\]/.exec(selector); + const parseResult = /(\w+)\[(-\d+|\d+)\]/.exec(selector); if (parseResult) { const [, objSelector, arraySelector] = parseResult; From 2ad9d6a4c24723aeffdd66e1a3fbc31568bc3e7a Mon Sep 17 00:00:00 2001 From: Long Jun Date: Fri, 15 Nov 2019 16:44:18 +0800 Subject: [PATCH 8/8] fix ze's comments --- .../visual-designer/src/editors/ObiEditor.tsx | 7 +++---- .../src/utils/cursorTracker/arrowMove.ts | 2 +- .../cursorTracker/calculate/calculateBySchema.ts | 13 +++++++++++-- .../cursorTracker/calculate/calculateByVector.ts | 4 +--- .../{calculate/index.ts => locateElement.ts} | 7 +++---- .../src/utils/cursorTracker/tabMove.ts | 2 +- .../visual-designer/src/utils/nodeOperation.ts | 7 +++++++ 7 files changed, 27 insertions(+), 15 deletions(-) rename Composer/packages/extensions/visual-designer/src/utils/cursorTracker/{calculate/index.ts => locateElement.ts} (90%) create mode 100644 Composer/packages/extensions/visual-designer/src/utils/nodeOperation.ts diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index ab9c43428f..02cf4b943b 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -22,10 +22,11 @@ import { pasteNodes, deleteNodes, } from '../utils/jsonTracker'; -import { moveCursor, querySelectableElements, SelectorElement } from '../utils/cursorTracker/index'; +import { moveCursor, querySelectableElements, SelectorElement } from '../utils/cursorTracker'; import { NodeIndexGenerator } from '../utils/NodeIndexGetter'; import { normalizeSelection } from '../utils/normalizeSelection'; import { KeyboardZone } from '../components/lib/KeyboardZone'; +import { scrollNodeIntoView } from '../utils/nodeOperation'; import { AdaptiveDialogEditor } from './AdaptiveDialogEditor'; @@ -280,9 +281,7 @@ export const ObiEditor: FC = ({ selectedIds: [selected as string], }); focused && onFocusSteps([focused], tab); - - document.querySelector(`[${AttrNames.SelectedId}="${selected}"]`) && - (document.querySelector(`[${AttrNames.SelectedId}="${selected}"]`) as Element).scrollIntoView(true); + scrollNodeIntoView(`[${AttrNames.SelectedId}="${selected}"]`); break; } case KeyboardPrimaryTypes.Operation: { diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts index 3495f15131..64edb49f25 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/arrowMove.ts @@ -3,7 +3,7 @@ import { KeyboardCommandTypes } from '../../constants/KeyboardCommandTypes'; -import { locateNearestElement } from './calculate/index'; +import { locateNearestElement } from './locateElement'; import { SelectorElement, Direction } from './type'; export function handleArrowkeyMove( diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts index 6400b0b121..09943b1427 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateBySchema.ts @@ -58,6 +58,11 @@ export function filterPromptElementsBySchema( candidateElements = elements.filter(ele => ele.tab !== PromptTab.OTHER); } break; + case Direction.Left: + if (currentElement.tab === PromptTab.OTHER) { + candidateElements = elements.filter(ele => ele.tab === PromptTab.USER_INPUT); + } + break; case Direction.Right: if (!currentElement.tab) { candidateElements = elements.filter(ele => ele.tab !== PromptTab.OTHER); @@ -77,11 +82,13 @@ function handleNextMoveFilter(currentElement: SelectorElement, elements: Selecto const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; const condition1 = eleSelectors.length === currentElementSelectors.length && + eleSelectors.slice(0, eleSelectors.length - 2).join('.') === + currentElementSelectors.slice(0, currentElementSelectors.length - 2).join('.') && Number(eleSelectors[eleSelectors.length - 1]) - 1 <= Number(currentElementSelectors[currentElementSelectors.length - 1]); const condition2 = eleSelectors.length > currentElementSelectors.length && - ele.selectedId.includes(currentElement.selectedId) && + eleSelectors.join('.').includes(currentElementSelectors.join('.')) && Number(eleSelectors[eleSelectors.length - 1]) === 0; const condition3 = eleSelectors.length < currentElementSelectors.length && @@ -96,6 +103,8 @@ function handlePrevMoveFilter(currentElement: SelectorElement, elements: Selecto const eleSelectors = parseSelector(transformDefaultBranch(ele.selectedId)) as string[]; const condition1 = eleSelectors.length === currentElementSelectors.length && + eleSelectors.slice(0, eleSelectors.length - 2).join('.') === + currentElementSelectors.slice(0, currentElementSelectors.length - 2).join('.') && Number(eleSelectors[eleSelectors.length - 1]) + 1 >= Number(currentElementSelectors[currentElementSelectors.length - 1]); const condition2 = @@ -104,7 +113,7 @@ function handlePrevMoveFilter(currentElement: SelectorElement, elements: Selecto Number(currentElementSelectors[currentElementSelectors.length - 1]); const condition3 = eleSelectors.length < currentElementSelectors.length && - currentElement.selectedId.includes(ele.selectedId) && + currentElementSelectors.join('.').includes(eleSelectors.join('.')) && Number(currentElementSelectors[currentElementSelectors.length - 1]) === 0; return condition1 || condition2 || condition3; }); diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts index 72e46bba85..d09209fbd9 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/calculateByVector.ts @@ -82,9 +82,7 @@ export function sortElementsByVector( ): SelectorElement[] { const { assistAxle, boundRectKey } = transformDirectionToVectorAttrs(direction); const elementVectors = calculateElementVector(currentElement, elements, boundRectKey, assistAxle); - const candidates = elementVectors.sort( - (ele1, ele2) => ele2.distance / ele2.assistDistance - ele1.distance / ele1.assistDistance - ); + const candidates = elementVectors.sort((ele1, ele2) => ele1.distance - ele2.distance); return transformVectorToElement(candidates, elements); } diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/locateElement.ts similarity index 90% rename from Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts rename to Composer/packages/extensions/visual-designer/src/utils/cursorTracker/locateElement.ts index 8c44a5d741..d857ef9831 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/calculate/index.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/locateElement.ts @@ -1,10 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { SelectorElement, Direction } from '../type'; - -import { filterElementsByVector, sortElementsByVector } from './calculateByVector'; -import { filterPromptElementsBySchema, filterElementBySchema } from './calculateBySchema'; +import { SelectorElement, Direction } from './type'; +import { filterElementsByVector, sortElementsByVector } from './calculate/calculateByVector'; +import { filterPromptElementsBySchema, filterElementBySchema } from './calculate/calculateBySchema'; /** * * @param currentElement current element diff --git a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts index cd91778afc..8db10f5a74 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/cursorTracker/tabMove.ts @@ -3,7 +3,7 @@ import { KeyboardCommandTypes } from '../../constants/KeyboardCommandTypes'; import { SelectorElement, Direction } from './type'; -import { locateNearestElement } from './calculate/index'; +import { locateNearestElement } from './locateElement'; function isParentRect(parentRect, childRect) { return ( diff --git a/Composer/packages/extensions/visual-designer/src/utils/nodeOperation.ts b/Composer/packages/extensions/visual-designer/src/utils/nodeOperation.ts new file mode 100644 index 0000000000..8eb376092a --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/nodeOperation.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export function scrollNodeIntoView(selector: string) { + const node: Element = document.querySelector(selector) as Element; + node.scrollIntoView(true); +}