Reputation: 7413
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;
}
Upvotes: 0
Views: 42
Reputation: 7413
I've adopted another approach by separating the panel code and populating it dynamically. I'm setting its position based on which line number is hover.
Upvotes: 0