Quantcast
Channel: Active questions tagged svelte - Stack Overflow
Viewing all articles
Browse latest Browse all 1545

How to Sync Scrolling between 2 containers while displaying panel on hover

$
0
0

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.

enter image description here

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>&nbsp;</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;}

Viewing all articles
Browse latest Browse all 1545

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>