Skip to content

Commit 6b9cd2f

Browse files
committed
chore: auto scroll to added variable
1 parent 100ad61 commit 6b9cd2f

File tree

2 files changed

+101
-8
lines changed

2 files changed

+101
-8
lines changed

packages/frontend/src/components/RichTextEditor/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
checkAutoFocus,
4747
genVariableInfoMap,
4848
getPopoverPlacement,
49+
singleLineEditorScroll,
4950
substituteOldTemplates,
5051
} from './utils'
5152

@@ -215,8 +216,12 @@ const Editor = ({
215216
},
216217
})
217218
editor?.commands.focus()
219+
220+
if (isMulticol && editor) {
221+
singleLineEditorScroll(editor)
222+
}
218223
},
219-
[editor],
224+
[editor, isMulticol],
220225
)
221226

222227
const {

packages/frontend/src/components/RichTextEditor/utils.ts

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { FieldValues, UseFormGetValues } from 'react-hook-form'
22
import { PlacementWithLogical } from '@chakra-ui/react'
33
import { Editor } from '@tiptap/react'
4-
import { HTMLElement, Node, parse, TextNode } from 'node-html-parser'
4+
import {
5+
HTMLElement as NodeHTMLElement,
6+
Node,
7+
parse,
8+
TextNode,
9+
} from 'node-html-parser'
510

611
import type { StepWithVariables } from '@/helpers/variables'
712

@@ -59,12 +64,12 @@ export function genVariableInfoMap(
5964
function constructVariableSpanElement(
6065
varInfo: VariableInfoMap,
6166
id: string,
62-
): HTMLElement {
67+
): NodeHTMLElement {
6368
const idComponents = id.split('.')
6469
const varInfoForNode = varInfo.get(`{{${id}}}`)
6570
const value = varInfoForNode?.testRunValue || ''
6671
const label = varInfoForNode?.label || idComponents[idComponents.length - 1]
67-
const span = new HTMLElement('span', {})
72+
const span = new NodeHTMLElement('span', {})
6873
span.setAttribute('data-type', 'variable')
6974
span.setAttribute('data-id', id)
7075
span.setAttribute('data-label', label)
@@ -93,9 +98,9 @@ function substituteTemplateStringWithSpan(
9398
}
9499

95100
function recursiveSubstitute(
96-
el: HTMLElement,
101+
el: NodeHTMLElement,
97102
varInfo: VariableInfoMap,
98-
): HTMLElement {
103+
): NodeHTMLElement {
99104
const dataIdAttr = el.getAttribute('data-id')
100105
const dataTypeAttr = el.getAttribute('data-type')
101106
if (dataTypeAttr === 'variable' && dataIdAttr != null) {
@@ -105,7 +110,7 @@ function recursiveSubstitute(
105110
}
106111
const newChildNodes: Node[] = []
107112
el.childNodes.forEach((n) => {
108-
if (n instanceof HTMLElement) {
113+
if (n instanceof NodeHTMLElement) {
109114
newChildNodes.push(recursiveSubstitute(n, varInfo))
110115
} else if (n instanceof TextNode) {
111116
// We cannot use n.textContent here because it will unescape all HTML tags
@@ -139,7 +144,7 @@ export function getPopoverPlacement(
139144
return 'bottom-start'
140145
}
141146

142-
const editorElement = editor?.view.dom as globalThis.HTMLElement
147+
const editorElement = editor?.view.dom as HTMLElement
143148
if (!editorElement) {
144149
return 'bottom-start'
145150
}
@@ -163,3 +168,86 @@ export const checkAutoFocus = (
163168
const isNewRow = rowData?.isNew
164169
return { shouldAutoFocus: isNewRow && autoFocusProp, isNewRow, rowData }
165170
}
171+
172+
// Add scrolling behavior for single-line mode
173+
export const singleLineEditorScroll = (editor: Editor) => {
174+
if (!editor) {
175+
return
176+
}
177+
178+
setTimeout(() => {
179+
const singleLineEditor = editor.view.dom.closest(
180+
'.single-line-editor',
181+
) as HTMLElement
182+
if (singleLineEditor) {
183+
// Get the current cursor position
184+
const pos = editor.state.selection.$head.pos
185+
let targetVariable = findClosestVariableNode(
186+
editor,
187+
pos,
188+
singleLineEditor,
189+
)
190+
191+
// If we still haven't found it, fall back to the last variable
192+
if (!targetVariable) {
193+
const variables = Array.from(
194+
singleLineEditor.getElementsByClassName('node-variable'),
195+
) as HTMLElement[]
196+
if (variables.length > 0) {
197+
targetVariable = variables[variables.length - 1]
198+
}
199+
}
200+
201+
// Scroll to the target variable if found
202+
if (targetVariable) {
203+
scrollVariableIntoView(targetVariable, singleLineEditor)
204+
}
205+
}
206+
}, 10)
207+
}
208+
209+
export function findClosestVariableNode(
210+
editor: any,
211+
pos: number,
212+
container: HTMLElement,
213+
): HTMLElement | null {
214+
for (let offset = -1; offset <= 1; offset++) {
215+
const checkPos = Math.max(0, pos + offset)
216+
const domInfo = editor.view.domAtPos(checkPos)
217+
const node = domInfo.node as HTMLElement
218+
219+
if (node.classList?.contains('node-variable')) {
220+
return node
221+
}
222+
223+
const prevSibling = node.previousElementSibling as HTMLElement
224+
if (prevSibling?.classList?.contains('node-variable')) {
225+
return prevSibling
226+
}
227+
228+
const nextSibling = node.nextElementSibling as HTMLElement
229+
if (nextSibling?.classList?.contains('node-variable')) {
230+
return nextSibling
231+
}
232+
}
233+
234+
// Fallback: last variable in the container
235+
const variables = container.getElementsByClassName('node-variable')
236+
return variables.length > 0
237+
? (variables[variables.length - 1] as HTMLElement)
238+
: null
239+
}
240+
241+
export function scrollVariableIntoView(
242+
target: HTMLElement,
243+
container: HTMLElement,
244+
) {
245+
const targetDiv = target
246+
const containerWidth = container.clientWidth
247+
const scrollLeft =
248+
targetDiv.offsetLeft - containerWidth + targetDiv.offsetWidth + 20
249+
container.scrollTo({
250+
left: Math.max(0, scrollLeft),
251+
behavior: 'smooth',
252+
})
253+
}

0 commit comments

Comments
 (0)