mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
fix(frontend): use custom caret position function (#5544)
* use custom caret position function * add try catch to not show the tooltip if any error occured * log error
This commit is contained in:
Generated
-7
@@ -62,7 +62,6 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"textarea-caret": "^3.1.0",
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@~11.1.2",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
@@ -11882,12 +11881,6 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/textarea-caret": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz",
|
||||
"integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
|
||||
@@ -138,7 +138,6 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"textarea-caret": "^3.1.0",
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@~11.1.2",
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { ContextElement } from './core'
|
||||
import AvailableContextList from './AvailableContextList.svelte'
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
|
||||
export let instructions: string
|
||||
export let availableContext: ContextElement[]
|
||||
@@ -22,6 +21,120 @@
|
||||
let textarea: HTMLTextAreaElement
|
||||
let selectedSuggestionIndex = 0
|
||||
|
||||
// Properties to copy for caret position calculation
|
||||
const properties = [
|
||||
'direction',
|
||||
'boxSizing',
|
||||
'width',
|
||||
'height',
|
||||
'overflowX',
|
||||
'overflowY',
|
||||
'borderTopWidth',
|
||||
'borderRightWidth',
|
||||
'borderBottomWidth',
|
||||
'borderLeftWidth',
|
||||
'borderStyle',
|
||||
'paddingTop',
|
||||
'paddingRight',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'fontWeight',
|
||||
'fontStretch',
|
||||
'fontSize',
|
||||
'fontSizeAdjust',
|
||||
'lineHeight',
|
||||
'fontFamily',
|
||||
'textAlign',
|
||||
'textTransform',
|
||||
'textIndent',
|
||||
'textDecoration',
|
||||
'letterSpacing',
|
||||
'wordSpacing',
|
||||
'tabSize',
|
||||
'MozTabSize'
|
||||
]
|
||||
|
||||
function getCaretCoordinates(element: HTMLTextAreaElement, position: number) {
|
||||
// Create mirror div
|
||||
const div = document.createElement('div')
|
||||
div.id = 'input-textarea-caret-position-mirror-div'
|
||||
document.body.appendChild(div)
|
||||
|
||||
// Set styles
|
||||
const style = div.style
|
||||
const computed = window.getComputedStyle(element)
|
||||
const isInput = element.nodeName === 'INPUT'
|
||||
|
||||
// Default textarea styles
|
||||
style.whiteSpace = 'pre-wrap'
|
||||
if (!isInput) style.wordWrap = 'break-word'
|
||||
|
||||
// Position off-screen
|
||||
style.position = 'absolute'
|
||||
style.visibility = 'hidden'
|
||||
|
||||
// Transfer properties
|
||||
properties.forEach(function (prop) {
|
||||
if (isInput && prop === 'lineHeight') {
|
||||
// Special case for inputs
|
||||
if (computed.boxSizing === 'border-box') {
|
||||
const height = parseInt(computed.height)
|
||||
const outerHeight =
|
||||
parseInt(computed.paddingTop) +
|
||||
parseInt(computed.paddingBottom) +
|
||||
parseInt(computed.borderTopWidth) +
|
||||
parseInt(computed.borderBottomWidth)
|
||||
const targetHeight = outerHeight + parseInt(computed.lineHeight)
|
||||
if (height > targetHeight) {
|
||||
style.lineHeight = height - outerHeight + 'px'
|
||||
} else if (height === targetHeight) {
|
||||
style.lineHeight = computed.lineHeight
|
||||
} else {
|
||||
style.lineHeight = '0'
|
||||
}
|
||||
} else {
|
||||
style.lineHeight = computed.height
|
||||
}
|
||||
} else {
|
||||
style[prop] = computed[prop]
|
||||
}
|
||||
})
|
||||
|
||||
// Firefox special handling
|
||||
const isFirefox =
|
||||
(window as typeof window & { mozInnerScreenX: number }).mozInnerScreenX != null
|
||||
if (isFirefox) {
|
||||
if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'
|
||||
} else {
|
||||
style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
// Add content before caret
|
||||
div.textContent = element.value.substring(0, position)
|
||||
|
||||
// Replace spaces with non-breaking spaces for input elements
|
||||
if (isInput) div.textContent = div.textContent.replace(/\s/g, '\u00a0')
|
||||
|
||||
// Create span for position calculation
|
||||
const span = document.createElement('span')
|
||||
span.textContent = element.value.substring(position) || '.'
|
||||
div.appendChild(span)
|
||||
|
||||
// Get coordinates
|
||||
const coordinates = {
|
||||
top: span.offsetTop + parseInt(computed['borderTopWidth']),
|
||||
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
|
||||
height: parseInt(computed['lineHeight'])
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(div)
|
||||
|
||||
return coordinates
|
||||
}
|
||||
|
||||
function getHighlightedText(text: string) {
|
||||
return text.replace(/@[\w/.-]+/g, (match) => {
|
||||
const contextElement = availableContext.find((c) => c.title === match.slice(1))
|
||||
@@ -57,40 +170,46 @@
|
||||
) {
|
||||
if (!textarea || !showContextTooltip) return
|
||||
|
||||
const coords = getCaretCoordinates(textarea, textarea.selectionEnd)
|
||||
const rect = textarea.getBoundingClientRect()
|
||||
try {
|
||||
const coords = getCaretCoordinates(textarea, textarea.selectionEnd)
|
||||
const rect = textarea.getBoundingClientRect()
|
||||
|
||||
const filteredAvailableContext = availableContext.filter(
|
||||
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
|
||||
)
|
||||
const filteredAvailableContext = availableContext.filter(
|
||||
(c) => !contextTooltipWord || c.title.toLowerCase().includes(contextTooltipWord.slice(1))
|
||||
)
|
||||
|
||||
const itemHeight = 28 // Estimated height of one item + gap (Button: p-1(8px) + text-xs(16px) = 24px; Parent: gap-1(4px) = 28px)
|
||||
const containerPadding = 8 // p-1 top + p-1 bottom = 4px + 4px = 8px
|
||||
const maxHeight = 192 + containerPadding // max-h-48 (192px) + containerPadding (8px)
|
||||
const itemHeight = 28 // Estimated height of one item + gap (Button: p-1(8px) + text-xs(16px) = 24px; Parent: gap-1(4px) = 28px)
|
||||
const containerPadding = 8 // p-1 top + p-1 bottom = 4px + 4px = 8px
|
||||
const maxHeight = 192 + containerPadding // max-h-48 (192px) + containerPadding (8px)
|
||||
|
||||
// Calculate uncapped height, subtract gap from last item as it's not needed
|
||||
const numItems = filteredAvailableContext.length
|
||||
let uncappedHeight =
|
||||
numItems > 0 ? numItems * itemHeight - 4 + containerPadding : containerPadding
|
||||
// Ensure height is at least containerPadding even if no items
|
||||
uncappedHeight = Math.max(uncappedHeight, containerPadding)
|
||||
// Calculate uncapped height, subtract gap from last item as it's not needed
|
||||
const numItems = filteredAvailableContext.length
|
||||
let uncappedHeight =
|
||||
numItems > 0 ? numItems * itemHeight - 4 + containerPadding : containerPadding
|
||||
// Ensure height is at least containerPadding even if no items
|
||||
uncappedHeight = Math.max(uncappedHeight, containerPadding)
|
||||
|
||||
const estimatedTooltipHeight = Math.min(uncappedHeight, maxHeight)
|
||||
const margin = 6 // Small margin between caret and tooltip
|
||||
const estimatedTooltipHeight = Math.min(uncappedHeight, maxHeight)
|
||||
const margin = 6 // Small margin between caret and tooltip
|
||||
|
||||
let finalY: number
|
||||
let finalY: number
|
||||
|
||||
if (isFirstMessage) {
|
||||
// Position below the caret line
|
||||
finalY = rect.top + coords.top + coords.height - 3
|
||||
} else {
|
||||
// Position above the caret line
|
||||
finalY = rect.top + coords.top - estimatedTooltipHeight - margin
|
||||
}
|
||||
if (isFirstMessage) {
|
||||
// Position below the caret line
|
||||
finalY = rect.top + coords.top + coords.height - 3
|
||||
} else {
|
||||
// Position above the caret line
|
||||
finalY = rect.top + coords.top - estimatedTooltipHeight - margin
|
||||
}
|
||||
|
||||
tooltipPosition = {
|
||||
x: rect.left + coords.left - 70,
|
||||
y: finalY
|
||||
tooltipPosition = {
|
||||
x: rect.left + coords.left - 70,
|
||||
y: finalY
|
||||
}
|
||||
} catch (error) {
|
||||
// Hide tooltip on any error related to position calculation
|
||||
console.error('Error updating tooltip position', error)
|
||||
showContextTooltip = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user