Amit Kumar Gupta
Amit Kumar Gupta

Reputation: 7413

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

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;
}

Upvotes: 0

Views: 42

Answers (1)

Amit Kumar Gupta
Amit Kumar Gupta

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

Related Questions