In my svelte component, I'm displaying line numbers for each line in text area. I've disabled the scrolling of line number. And manually sync line number container when I scroll on text area. overflow-y: hidden;
on #line-numbers
play crucial role in this case.Now I'm adding another feature of showing a panel when I hover on a line number. This works well, if I add overflow: visible;
to #line-numbers
but then scrolling is not in sync.
Here is the code of Svelte Component
<script> import { getSelectedLines } from './selection.js'; import { handleEditing } from './text-editor.js'; import { onMount, afterUpdate } from 'svelte'; import './TextArea.css'; // Props export let text = ''; // The text content of the textarea export let placeholder = ''; // Optional placeholder text export let disabled = false; // Whether the textarea is disabled export let stepsExecutionTimes = []; // Execution time data export let mode = "editor"; // editor, monitor // Events import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let previousText = ''; // Track previous text for change detection let lineNumbers = []; // Array to hold line numbers let highlightedLines = []; // Array to hold highlighted lines let collapsedLines = new Set(); // Set to hold collapsed lines let lineNumbersContainer; // Reference to the line numbers container let textarea; // Reference to the textarea element let highlightedTextContainer; // Reference to the highlighted text container // Emit text change events function emitTextChange(newText) { dispatch('textChange', { text: newText }); } // Emit line selection events function emitLineSelection(selectedLines) { dispatch('lineSelection', { selectedLines }); } // Handle key down events for text editing and highlighting function handleKeyDown(event) { //.. } // Highlight the current step node based on the selected lines function highlightCurrentStepNode(event) { const selectedLines = getSelectedLines(event.target, event.key, event.shiftKey); emitLineSelection(selectedLines); // Emit selected lines highlightedLines = selectedLines.nodeIds; } // Handle key up events to detect text changes function handleKeyUp(event) { //.. } // Handle click events to highlight the current step function handleClick(event) { //.. } // Update line numbers based on the text content function updateLineNumbers() { const lines = text.split('\n'); let lineCount = 0; lineNumbers = lines.map((line, index) => { const trimmedLine = line.trim(); //TODO: exclude header lines if (trimmedLine.startsWith("FLOW:")) { lineCount = -1; // Reset line count for new FLOW return null; // No line number for FLOW line } else if (trimmedLine.length === 0) { return null; // No line number for empty lines } else { lineCount++; let className = ""; const exeTime = $stepsExecutionTimes.find(et => et.id === lineCount); //.. return { lineNumber: lineCount, className, exeTime }; } }); } function handleToggleDebug() { dispatch('toggleDebug'); } // Handle logs button click function handleShowLogs() { dispatch('showLogs'); } // Sync scrolling between textarea and line numbers function syncScroll(event) { const {scrollTop,scrollLeft} = event.target; lineNumbersContainer.scrollTop = scrollTop; highlightedTextContainer.scrollTop = scrollTop; highlightedTextContainer.scrollLeft = scrollLeft; if(highlightedTextContainer.scrollTop !== scrollTop){ event.target.scrollTop = highlightedTextContainer.scrollTop; lineNumbersContainer.scrollTop = highlightedTextContainer.scrollTop; } // event.preventDefault(); } // Prevent scrolling on line numbers function preventLineNumberScroll(event) { if (lineNumbersContainer) { lineNumbersContainer.scrollTop = event.target.scrollTop; } } // Highlight keywords, comments, and FLOW lines function highlightKeywords(text) { const lines = text.split('\n'); const highlightedLines = lines.map((line) => { if (line.trim().startsWith('#')) { // Highlight comment lines return `<span class="comment">${line}</span>`; } else if (line.trim().startsWith('FLOW:')) { // Highlight FLOW lines return `<span class="flow">${line}</span>`; } else { // Highlight other keywords const keywords = { branch: ["IF", "ELSE_IF", "ELSE", "LOOP"], leaving: ["GOTO", "SKIP", "STOP", "END"], normal: ["AND", "THEN", "BUT", "FOLLOW", "ERR"], other: ["FLOW", "FOLLOW"] }; let highlightedLine = line; Object.entries(keywords).forEach(([category, words]) => { words.forEach(word => { const regex = new RegExp(`\\b${word}\\b`, 'g'); highlightedLine = highlightedLine.replace(regex, `<span class="keyword ${category}">${word}</span>`); }); }); return `<span class="steptext">${highlightedLine}</span>`; } }); return highlightedLines.join('\n'); } // Update the highlighted text container function updateHighlightedText() { if (highlightedTextContainer) { highlightedTextContainer.innerHTML = highlightKeywords(text); } } $: { //Text is not being displayed on load without this code until user click on text area if (text !== previousText) { updateLineNumbers(); updateHighlightedText(); } } // Initialize line numbers on mount onMount(() => { updateLineNumbers(); updateHighlightedText(); }); // Update line numbers after the component updates (e.g., when switching flows) afterUpdate(() => { updateLineNumbers(); updateHighlightedText(); });</script><div id="text-area-container"><div id="line-numbers" bind:this={lineNumbersContainer} on:scroll={preventLineNumberScroll}> {#each lineNumbers as line, index} {#if line !== null}<div class:highlighted={highlightedLines.includes(String(line.lineNumber))} class={line.className}> {line.lineNumber}<!-- Panel for this line number --><div class="panel"><div>Min: {line.exeTime?.minExeTime || 0} ms</div><div>Avg: {line.exeTime?.avgExeTime || 0} ms</div><div>Max: {line.exeTime?.maxExeTime || 0} ms</div><div class="panel-icons"><button on:click={handleToggleDebug}>🔧</button><button on:click={handleShowLogs}>📄</button></div></div></div> {:else}<div> </div> {/if} {/each}</div><div id="highlighted-text" bind:this={highlightedTextContainer}></div><textarea id="text-area" bind:this={textarea} bind:value={text} {placeholder} {disabled} on:keyup={handleKeyUp} on:keydown={handleKeyDown} on:mouseup={handleClick} on:scroll={syncScroll} /></div>
Here is the CSS file
#text-area-container { display: flex; width: 100%; height: calc(100vh - 180px); font-family: monospace; background-color: #f9f9f9; overflow: hidden; /* Prevent double scrollbars */ position: relative;}#highlighted-text, #text-area, #line-numbers { font-family: monospace; font-size: 14px; /* Ensure consistent font size */ line-height: 1.5; /* Ensure consistent line height */}#line-numbers { position: relative; width: 40px; padding: 10px 5px; text-align: right; border-right: 1px solid #ccc; background-color: #f0f0f0; user-select: none; overflow-y: hidden; /* Disable scrolling */ height: 100%; /* Match height of textarea */ z-index: 3; overflow: visible;}#highlighted-text { color: inherit; position: absolute; top: 0; left: 40px; right: 0; bottom: 0; padding: 10px; box-sizing: border-box; height: 100%; pointer-events: none; /* Allow clicks to pass through to the textarea */ white-space: pre-wrap; font-family: monospace; overflow: hidden; z-index: 2; /* Ensure highlighted text is above the textarea */}#text-area { flex: 1; border: 0; padding: 10px; box-sizing: border-box; resize: none; height: 100%; background-color: transparent; overflow-y: auto; /* Enable scrolling */ height: 100%; /* Match height of line numbers */ position: relative; z-index: 1; /* Ensure textarea is below the highlighted text */ color: transparent; /* Make textarea text transparent */ caret-color: black; /* Ensure the caret (cursor) is visible */}#text-area:disabled { background-color: #eee; cursor: not-allowed;}#line-numbers > div { position: relative; /* Ensure panels are positioned relative to the line number */ padding-right: 10px; /* Add space for the panel */}/* Panel */.panel { position: absolute; top: 0; left: 100%; /* Position to the right of the line number */ background-color: white; border: 1px solid #ccc; padding: 10px; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 3; /* Ensure panel is above other elements */ pointer-events: none; /* Allow interactions with the panel */ opacity: 0; /* Hidden by default */ transition: opacity 0.2s ease-in-out; min-width: 80px; white-space: nowrap; /* Prevent line breaks */}/* Show panel on hover */#line-numbers > div:hover .panel { opacity: 1; /* Visible when hovered */ pointer-events: auto; }/* Panel icons */.panel-icons { display: flex; gap: 10px; margin-top: 10px;}.panel-icons button { background: none; border: 1px solid #ccc; cursor: pointer; font-size: 14px; padding: 5px 10px; border-radius: 4px;}.panel-icons button:hover { background-color: #f0f0f0;}