310X-Word Replacer WTRLAB

Use it however, but the notes feature is fragile

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         310X-Word Replacer WTRLAB
// @match        https://wtr-lab.com/en/*
// @version      31.1
// @namespace    WordReplace310XXXX
// @description  Use it however, but the notes feature is fragile
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = 'wordReplacerPairsV3';

  // Load data or initialize empty object
  let data = loadData();

  // Main UI button
const mainButton = document.createElement('button');
mainButton.textContent = 'Word Replacer';
Object.assign(mainButton.style, {
  position: 'fixed',
  bottom: '20px',       // always 20px from bottom
  right: '20px',
  zIndex: '100001',     // above everything except popup panel
  padding: '8px 14px',
  fontSize: '16px',
  backgroundColor: '#333',
  color: '#fff',
  border: 'none',
  borderRadius: '6px',
  cursor: 'pointer',
});
document.body.appendChild(mainButton);

  let popup = null;

  mainButton.addEventListener('click', () => {
    if (popup) {
      closePopup();
    } else {
      openPopup();
      replaceTextInChapter();
    }
  });

  function loadData() {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return {};
    try {
      return JSON.parse(raw);
    } catch {
      return {};
    }
  }

  function saveData(obj) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
  }
function closePopup() {
  if (popup) {
    popup.remove();
    popup = null;
  }
}
  // Escape regex helper
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\",]/g, '\\$&');



// Check if index is start of sentence
function isStartOfSentence(index, fullText) {
  if (index === 0) return true; // start of paragraph/text

  const before = fullText.slice(0, index);

  // If the slice is only whitespace, treat as start
  if (/^\s*$/.test(before)) return true;

  // Trim trailing spaces for checks of punctuation/endings
  const trimmed = before.replace(/\s+$/, '');

  // 1) sentence-ending punctuation (., !, ?, …) possibly followed by closing quotes/brackets:
  if (/[.!?…]["”’')\]]*$/.test(trimmed)) return true;

  // 2) newline right before (dialogue line breaks etc.)
  if (/[\n\r]\s*$/.test(before)) return true;

  // 3) opening quote/paren immediately before the match OR opening quote then space
  if (/["“”'‘(\[]\s*$/.test(before)) return true;

  // 4) after any **double quotation mark** (straight or curly) **without space**
  if (/["“”]$/.test(before)) return true;

  // 5) after "Chapter XX:" optionally followed by a comma and space
  if (/Chapter\s+\d+:\s*,?\s*$/.test(before)) return true;

  return false;
}

function isInsideDialogueAtIndex(htmlText, index) {
  const leftQuotes = `"“‘`;   // opening quotes
  const rightQuotes = `"”’`;  // closing quotes
  let quoteStack = [];

  // Search left from the start to the index
  for (let i = 0; i < index; i++) {
    // Stop counting at paragraph or line break
    if (htmlText.slice(i).match(/^<\s*(?:p|br)[^>]*>/i)) break;

    const char = htmlText[i];
    if (leftQuotes.includes(char)) {
      quoteStack.push(char);
    } else if (rightQuotes.includes(char)) {
      // Pop matching opening quote if exists
      if (quoteStack.length > 0) quoteStack.pop();
    }
  }

  return quoteStack.length > 0; // true = inside dialogue
}


  // Preserve first capital helper
  function applyPreserveCapital(orig, replacement) {
    if (!orig) return replacement;
    if (orig[0] >= 'A' && orig[0] <= 'Z') {
      return replacement.charAt(0).toUpperCase() + replacement.slice(1);
    }
    return replacement;
  }

function buildIgnoreRegex(from, ignoreTerm, entry, wildcardSymbol) {
    const flags = entry.ignoreCapital ? 'gi' : 'g';
    let basePattern = escapeRegex(from).replace(new RegExp(`\\${wildcardSymbol}`, 'g'), '.');

    if (entry.noTrailingSpace) {
        basePattern = basePattern.trim();
    }

    if (ignoreTerm) {
        // Negative lookahead for the ignore phrase (allowing spaces or punctuation between)
        return new RegExp(
            basePattern + `(?![\\s"“”'’,.-]+${escapeRegex(ignoreTerm)})`,
            flags
        );
    } else {
        return new RegExp(basePattern, flags);
    }
}
  // The core replacement function, applies all enabled replacements with flags
function applyReplacements(text, replacements) {
  let replacedText = text;
  const WILDCARD = '@';
  // Expanded boundary characters
const punctuationRegex = /^[\W_'"“”‘’„,;:!?~()\[\]{}<>【】「」『』()《》〈〉—–-]|[\W_'"“”‘’„,;:!?~()\[\]{}<>【】「」『』()《》〈〉—–-]$/;
  const quoteChars = `"'“”‘’`;

  for (const entry of replacements) {
    if (!entry.from || !entry.to) continue;

    const flags = entry.ignoreCapital ? 'gi' : 'g';
    let searchTerm = entry.from;
    if (entry.noTrailingSpace) searchTerm = searchTerm.trimEnd();

    // Handle optional ignore term |term| (before or after)
    let ignoreTerm = null;
    const prefixMatch = searchTerm.match(/^\|(.*?)\|\s*(.+)$/);
    const suffixMatch = searchTerm.match(/^(.*?)\s*\|(.*?)\|$/);
    if (prefixMatch) {
      ignoreTerm = { type: 'before', value: prefixMatch[1] };
      searchTerm = prefixMatch[2];
    } else if (suffixMatch) {
      ignoreTerm = { type: 'after', value: suffixMatch[2] };
      searchTerm = suffixMatch[1];
    }

    // PATCH: allow any quote char at start if searchTerm starts with a quote
    if (quoteChars.includes(searchTerm.charAt(0))) {
      searchTerm = `[${quoteChars}]` + escapeRegex(searchTerm.slice(1));
    } else {
      searchTerm = escapeRegex(searchTerm);
    }

    // Placeholder ^ handling
    const caretCountFrom = (entry.from.match(/\^/g) || []).length;
    const caretCountTo = (entry.to.match(/\^/g) || []).length;
    const usePlaceholder = caretCountFrom === 1 && caretCountTo === 1;
    let base = usePlaceholder ? searchTerm.replace('\\^', '([^\\s])') : searchTerm.replace(new RegExp(`\\${WILDCARD}`, 'g'), '.');

    // Determine boundaries
    const firstChar = entry.from.charAt(0);
    const lastChar = entry.from.charAt(entry.from.length - 1);
    const skipBoundaries = punctuationRegex.test(firstChar) || punctuationRegex.test(lastChar);

let patternStr = (entry.allInstances || skipBoundaries)
  ? base
  : `(?<=^|[^A-Za-z0-9])${base}(?=[^A-Za-z0-9]|$)`;

    // Apply ignore-term properly
    if (ignoreTerm && ignoreTerm.value) {
      const escapedIgnore = escapeRegex(ignoreTerm.value);
      if (ignoreTerm.type === 'before') {
        patternStr = `(?<!${escapedIgnore})${patternStr}`;
      } else if (ignoreTerm.type === 'after') {
        patternStr = `${patternStr}(?!${escapedIgnore}\\s*)`; // allow trailing spaces
      } else {
        patternStr = `(?<!${escapedIgnore})${patternStr}(?!${escapedIgnore}\\s*)`;
      }
    }

    const regex = new RegExp(patternStr, flags);

    let newText = '';
    let lastIndex = 0;
    let match;

    while ((match = regex.exec(replacedText)) !== null) {
      const idx = match.index;
      const insideDialogue = isInsideDialogueAtIndex(replacedText, idx);
      if ((entry.insideDialogueOnly && !insideDialogue) || (entry.outsideDialogueOnly && insideDialogue)) continue;

      newText += replacedText.slice(lastIndex, idx);

      let replacementBase = entry.noTrailingSpace ? entry.to.trimEnd() : entry.to;
      if (usePlaceholder && match[1] !== undefined) replacementBase = replacementBase.replace('^', match[1]);

      // START OF SENTENCE capitalization check
      const startSentence = entry.startOfSentence && isStartOfSentence(idx, replacedText);
      const finalReplacement = startSentence
        ? (entry.preserveFirstCapital
            ? applyPreserveCapital(match[0], replacementBase)
            : replacementBase.charAt(0).toUpperCase() + replacementBase.slice(1))
        : (entry.preserveFirstCapital
            ? applyPreserveCapital(match[0], replacementBase)
            : replacementBase);

      newText += finalReplacement;
      lastIndex = idx + match[0].length;
    }

    if (lastIndex < replacedText.length) newText += replacedText.slice(lastIndex);
    replacedText = newText;
  }

  return replacedText;
}

  // The main replaceTextInChapter, preserves old proportional slicing and paragraph-limited replacement
function replaceTextInChapter() {
  const seriesId = (() => {
    const urlMatch = location.href.match(/\/novel\/(\d+)\//i);
    if (urlMatch) return urlMatch[1];
    const crumb = document.querySelector('.breadcrumb li.breadcrumb-item a[href*="/novel/"]');
    if (crumb) {
      const crumbMatch = crumb.href.match(/\/novel\/(\d+)\//i);
      if (crumbMatch) return crumbMatch[1];
    }
    return null;
  })();

  let replacements = [];
  for (const key in data) {
    if (key === 'global' || (seriesId && key === `series-${seriesId}`)) {
      replacements = replacements.concat(data[key].filter(e => e.enabled));
    }
  }

  if (replacements.length === 0) return false;

  const paragraphs = document.querySelectorAll(
    'div.chapter-body p[data-line], h3.chapter-title, div.post-content *'
  );

  let replacedAny = false;

  paragraphs.forEach(p => {
    const textNodes = [];
    const originalLengths = [];
    let originalText = '';

    const walker = document.createTreeWalker(p, NodeFilter.SHOW_TEXT, null, false);
    while (walker.nextNode()) {
      const node = walker.currentNode;
      if (!node.nodeValue) continue;
      textNodes.push(node);
      originalLengths.push(node.nodeValue.length);
      originalText += node.nodeValue;
    }

    if (!originalText) return;

    // --- Step 1: Apply all replacements on flat text ---
    let replacedText = applyReplacements(originalText, replacements);

    // --- Step 2: Proportional slicing back into text nodes ---
    const totalOriginalLength = originalText.length;
    const totalReplacedLength = replacedText.length;
    let currentIndex = 0;

    textNodes.forEach((node, i) => {
      const proportion = originalLengths[i] / totalOriginalLength;
      let sliceLength = Math.round(proportion * totalReplacedLength);
      if (i === textNodes.length - 1) sliceLength = totalReplacedLength - currentIndex;
      node.nodeValue = replacedText.slice(currentIndex, currentIndex + sliceLength);
      currentIndex += sliceLength;
    });

    // --- Step 3: Wrap note entries in <span> ---
    textNodes.forEach(node => {
      let nodeVal = node.nodeValue;
      replacements.forEach(entry => {
        if (entry.note && entry.note.trim()) {
          let idx = 0;
          while ((idx = nodeVal.indexOf(entry.to, idx)) !== -1) {
            const parent = node.parentNode;
            if (!parent) break;

            const span = document.createElement('span');
            span.className = 'text-patch system_term';
            span.dataset.note = entry.note;
            span.textContent = entry.to;

            const before = nodeVal.slice(0, idx);
            const after = nodeVal.slice(idx + entry.to.length);

            if (before) parent.insertBefore(document.createTextNode(before), node);
            parent.insertBefore(span, node);
            nodeVal = after;
            idx = 0; // restart in remaining text
            node.nodeValue = nodeVal;
            if (!nodeVal) {
              parent.removeChild(node);
              break;
            }
          }
        }
      });
    });

    replacedAny = true;
  });

  if (replacedAny) console.log('Replacements done on chapter paragraphs.');
  return replacedAny;
}


function wrapNotesInParagraph(paragraph, replacements) {
  const html = paragraph.innerHTML;

  let newHTML = html;

  replacements.forEach(rep => {
    if (rep.note && rep.note.trim()) {
      // Regex escape
      const fromEscaped = rep.from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      // Wrap the entire matching text in a span with data-note
      const regex = new RegExp(`(${fromEscaped})`, 'gi');
      newHTML = newHTML.replace(regex, `<span class="text-patch system_term" data-note="${rep.note}">$1</span>`);
    }
  });

  paragraph.innerHTML = newHTML;
}

function runReplacementMultiple(times = 1, delay = 100) {
  let count = 0;
// ---------------------
// Single global note modal
// ---------------------

function initInlinePopoverButtons() {
  const seriesId = window.seriesId || 'default-series';
  const shownotesRaw = localStorage.getItem('shownotes') || '';
  const shownotesSet = new Set(shownotesRaw.split(',').filter(Boolean));
  const notesVisible = shownotesSet.has(seriesId); // only visible if enabled for this series

  document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
    // Skip if we already initialized a button for this span
    if (span.dataset.hasPencil === 'true') return;

    // Mark as having a pencil
    span.dataset.hasPencil = 'true';

    // Create tiny pencil button
    const btn = document.createElement('button');
    btn.textContent = '✎';
    btn.title = 'Show Note';
    Object.assign(btn.style, {
      fontSize: '10px',
      padding: '0 2px',
      marginLeft: '4px',
      cursor: 'pointer',
      lineHeight: '1',
      verticalAlign: 'middle',
      border: 'none',
      background: 'transparent',
      display: notesVisible ? 'inline' : 'none', // invisible by default
    });

    // Insert after the span
    span.insertAdjacentElement('afterend', btn);

    // Create popover
    const pop = document.createElement('div');
    pop.className = 'user-popover';
    Object.assign(pop.style, {
      position: 'absolute',
      zIndex: 9999,
      maxWidth: '280px',
      background: 'black',
      color: 'white',
      border: '1px solid #ccc',
      borderRadius: '4px',
      boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
      padding: '8px',
      display: 'none',
    });

    const desc = document.createElement('div');
    desc.className = 'patch-desc';
    desc.textContent = 'Note: ' + span.dataset.note;
    pop.appendChild(desc);
    document.body.appendChild(pop);

    // Store reference on span
    span._popover = pop;

    // Button click toggles popover
    btn.addEventListener('click', e => {
      e.stopPropagation();
      document.querySelectorAll('.user-popover').forEach(p => {
        if (p !== pop) p.style.display = 'none';
      });

      desc.textContent = 'Note: ' + span.dataset.note;

      const rect = span.getBoundingClientRect();
      // Position below the span
      pop.style.top = `${window.scrollY + rect.bottom + 5}px`; // 5px gap below
      pop.style.left = `${window.scrollX + rect.left}px`;

      // Toggle visibility
      pop.style.display = pop.style.display === 'block' ? 'none' : 'block';
    });
  });

  // Click outside -> hide all popovers
  document.addEventListener('click', () => {
    document.querySelectorAll('.user-popover').forEach(p => (p.style.display = 'none'));
  });
}


function removeExtraPencils() {
  document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
    // find all button siblings immediately after this span
    const siblings = [];
    let next = span.nextElementSibling;

    while (next && next.tagName === 'BUTTON') {
      if (next.textContent.trim() === '✎') siblings.push(next);
      next = next.nextElementSibling;
    }

    // keep only the first ✎, remove the rest
    siblings.slice(1).forEach(btn => btn.remove());
  });
}

function nextPass() {
  const replaced = replaceTextInChapter();
  initInlinePopoverButtons();
  count++;
  if (count < times) {
    setTimeout(nextPass, delay);
  } else {
    // ✅ all passes finished, wait a bit then clean up
    setTimeout(removeExtraPencils, 150);
  }
}

nextPass(); // start first pass immediately
}


setTimeout(() => runReplacementMultiple(1, 100), 2000);



// Reactive URL detection (History API hook)
(function () {
  let lastUrl = location.href;

  function checkUrlChange() {
    const currentUrl = location.href;
    if (currentUrl !== lastUrl) {
      lastUrl = currentUrl;
      runReplacementMultiple(1, 100); // multi-pass on SPA navigation
      removeExtraPencils();
    }
  }

  // Wrap pushState
  const originalPushState = history.pushState;
  history.pushState = function () {
    originalPushState.apply(this, arguments);
    window.dispatchEvent(new Event("locationchange"));
  };

  // Wrap replaceState
  const originalReplaceState = history.replaceState;
  history.replaceState = function () {
    originalReplaceState.apply(this, arguments);
    window.dispatchEvent(new Event("locationchange"));
  };

  // Listen to Back/Forward
  window.addEventListener("popstate", () => window.dispatchEvent(new Event("locationchange")));


  // Unified listener
  window.addEventListener("locationchange", checkUrlChange);

  // ✅ Forcefully run once immediately on reload
  runReplacementMultiple(1, 100);
})();

  // ===============
  // UI popup code
  // ===============

function openPopup() {
  if (popup) return; // Prevent multiple

popup = document.createElement('div');
Object.assign(popup.style, {
  position: 'fixed',
  bottom: '70px',       // push above the main button
  right: '10px',
    width: '100vw',       // scale to viewport
   maxWidth: '370px',   // cap at your original
  height: 'auto',
  maxHeight: 'none',
  backgroundColor: '#fff',
  border: '1px solid #aaa',
  padding: '15px',
  boxShadow: '0 0 15px rgba(0,0,0,0.2)',
  overflow: 'visible',
  zIndex: '100000',     // below the button so button stays clickable
  fontFamily: 'Arial, sans-serif',
  fontSize: '14px',
});
document.body.appendChild(popup);

  // Toggle button for the list
  const toggleListBtn = document.createElement('button');
  toggleListBtn.textContent = 'Show List';
  toggleListBtn.style.marginBottom = '1px';
  toggleListBtn.style.display = 'block';
  toggleListBtn.style.color = 'black'
  styleButton(toggleListBtn);
  popup.appendChild(toggleListBtn);
// Info button
const infoBtn = document.createElement('button');
infoBtn.textContent = 'Info';              // Label it properly
infoBtn.style.marginLeft = '6px';          // space from the list button
infoBtn.style.padding = '5px 10px';        // optional: smaller padding if needed
infoBtn.style.alignSelf = 'flex-start';
infoBtn.style.color = 'black';    // <-- shift up to top of flex container
styleButton(infoBtn);

// Container for buttons
const topBtnContainer = document.createElement('div');
topBtnContainer.style.display = 'flex';
topBtnContainer.style.alignItems = 'center';
topBtnContainer.appendChild(toggleListBtn);
topBtnContainer.appendChild(infoBtn);
popup.appendChild(topBtnContainer);

// Info box (hidden by default)
const infoBox = document.createElement('div');
Object.assign(infoBox.style, {
  maxHeight: '0',
  overflow: 'hidden',
  backgroundColor: '#fff',
  color: 'black',
  border: '1px solid #000',
  padding: '0 10px',
  marginTop: '10px',
  fontSize: '13px',
  maxHeightWhenOpen: '200px',
  lineHeight: '1.4',
    overflowY: 'auto',
  transition: 'max-height 0.3s ease, padding 0.3s ease',
});
infoBox.innerHTML = `
  <div style="padding:10px 0;">
    <strong>Replacement System Info:</strong>
    <ul style="margin:5px 0; padding-left:18px;">
      <li><strong>Ignore Capital:</strong> Match case-insensitively.</li>
      <li><strong>Start of Sentence:</strong> Only capitalize if the word starts a sentence.</li>
      <li><strong>Fuzzy Match:</strong> Ignore boundaries, match anywhere.</li>
      <li><strong>Preserve Capital:</strong> Keep first letter capitalized if original was capitalized.</li>
      <li><strong>No Trailing Space:</strong> Trim trailing space in replacement.</li>
      <li><strong>Inside Dialogue Only:</strong> Replace only inside quotation marks.</li>
      <li><strong>Outside Dialogue Only:</strong> Replace only outside quotation marks.</li>
      <li><strong>Global:</strong> Makes the entry apply to all novels.</li>
      <li><strong>|ignore this|:</strong> Use before or after a word to ignore specific matches. Example: <code>|ignore |term</code> or <code>term| ignore|</code>. Spaces must be inside the <code>||</code>.</li>
      <li><strong>@ wildcard:</strong> Any character substitution. Example: <code>fr@t</code> replaces fret, frat, frit, etc.</li>
      <li><strong>^ special placeholder:</strong> Use <code>^</code> in Find like <code>Th^t</code> and in Replace like <code>Br^</code>. The character at <code>^</code> in Find will be preserved in the replacement.</li>
      <li><strong>Edit Entries:</strong> Use 'Show List', tap an entry to make edits and change the series ID. By default, it will be applied only to whatever novel you're on currently. If you entered a term while in Library, it will default to an empty series ID, which is global.</li>
	  <li>The Show Note requires you have go to the term editor in the Show List, but this is a fragile feature, I don't recommend it.<li>
    </ul>
  </div>
`;

popup.appendChild(infoBox);

// Info button click
infoBtn.addEventListener('click', (e) => {
  if (infoBox.style.maxHeight && infoBox.style.maxHeight !== '0px') {
    infoBox.style.maxHeight = '0';
    infoBox.style.padding = '0 10px';
  } else {
    infoBox.style.maxHeight = '200px';  // fixed max height
    infoBox.style.padding = '10px';
  }
  e.stopPropagation();
});

// Close info box if clicking outside
document.addEventListener('click', (e) => {
  if (!infoBox.contains(e.target) && e.target !== infoBtn) {
    infoBox.style.maxHeight = '0';
    infoBox.style.padding = '0 10px';
  }
});

// Create invert colors button
const invertBtn = document.createElement('button');
invertBtn.textContent = 'Invert Colors';
invertBtn.style.marginLeft = '6px';       // spacing from previous button
invertBtn.style.padding = '5px 10px';
invertBtn.style.alignSelf = 'flex-start';
invertBtn.style.color = 'black';
styleButton(invertBtn);
topBtnContainer.appendChild(invertBtn);

// Track state, restore from localStorage
let isInverted = localStorage.getItem('replacementUIInverted') === 'true';

// Function to apply inversion to popup and infoBox
function applyInversion(state) {
  isInverted = state;
  localStorage.setItem('replacementUIInverted', state); // persist across reloads

  if (isInverted) {
    popup.style.backgroundColor = '#000';
    popup.style.color = '#fff';

    infoBox.style.backgroundColor = '#000';
    infoBox.style.color = '#fff';

    Array.from(popup.querySelectorAll('*')).forEach(el => {
      if (['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
        el.style.backgroundColor = '#222';
        el.style.color = '#fff';
      }
      if (el.tagName === 'BUTTON') {
        el.style.backgroundColor = '#333';
        el.style.color = '#fff';
      }
    });
  } else {
    popup.style.backgroundColor = '#fff';
    popup.style.color = '#000';

    infoBox.style.backgroundColor = '#fff';
    infoBox.style.color = '#000';

    Array.from(popup.querySelectorAll('*')).forEach(el => {
      if (!['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(el.tagName)) {
        el.style.backgroundColor = '';
        el.style.color = '';
      } else {
        // revert form elements and buttons to default
        if (el.tagName === 'BUTTON') {
          el.style.backgroundColor = '#eee';
          el.style.color = 'black';
        }
        if (['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
          el.style.backgroundColor = '#fff';
          el.style.color = '#000';
        }
      }
    });
  }
}

// Apply last saved state when popup opens
applyInversion(isInverted);

// Toggle inversion on click
invertBtn.addEventListener('click', () => {
  applyInversion(!isInverted);
});
// Toggle inversion on click
invertBtn.addEventListener('click', () => {
  applyInversion(!isInverted);
});
// === Show/Hide Notes Button ===
const notesToggleBtn = document.createElement('button');
notesToggleBtn.textContent = 'Show Note'; // initial text
notesToggleBtn.style.marginLeft = '4px';  // spacing from previous button
  notesToggleBtn.style.padding = '2px 4px';

// Apply consistent button style
styleButton(notesToggleBtn);

// Append next to Invert Colors
topBtnContainer.appendChild(notesToggleBtn);

const seriesId = window.seriesId || 'default-series';
const shownotesRaw = localStorage.getItem('shownotes') || '';
const shownotesSet = new Set(shownotesRaw.split(',').filter(Boolean));
let notesInitiallyVisible = shownotesSet.has(seriesId);

// Helper: show/hide ✎ only for this series
function updatePencils(show) {
  document.querySelectorAll('span.text-patch.system_term[data-note]').forEach(span => {
    const btn = span.nextElementSibling;
    if (!btn || btn.textContent.trim() !== '✎') return;

    const noteSeries = span.dataset.series || 'default-series';
    if (noteSeries !== seriesId) return;

    btn.style.display = show ? 'inline-block' : 'none';
  });
}

// Initialize visibility
updatePencils(notesInitiallyVisible);
notesToggleBtn.textContent = notesInitiallyVisible ? 'Hide Note' : 'Show Note';

// Click handler
notesToggleBtn.addEventListener('click', () => {
  const showing = notesToggleBtn.textContent === 'Hide Note';
  const newShow = !showing;

  updatePencils(newShow);
  notesToggleBtn.textContent = newShow ? 'Hide Note' : 'Show Note';

  if (newShow) shownotesSet.add(seriesId);
  else shownotesSet.delete(seriesId);

  localStorage.setItem('shownotes', Array.from(shownotesSet).join(','));
});

// Participate in inversion
function applyInversion(state) {
  isInverted = state;
  localStorage.setItem('replacementUIInverted', state);

  const color = isInverted ? '#fff' : '#000';
  const bg = isInverted ? '#333' : '#eee';

  Array.from(topBtnContainer.querySelectorAll('button')).forEach(btn => {
    btn.style.color = color;
    btn.style.backgroundColor = bg;
  });

  popup.style.backgroundColor = isInverted ? '#000' : '#fff';
  popup.style.color = color;
  infoBox.style.backgroundColor = isInverted ? '#000' : '#fff';
  infoBox.style.color = color;
}

// Apply last saved state
applyInversion(isInverted);

// Append button next to Invert Colors
topBtnContainer.appendChild(notesToggleBtn);



const rulesContainer = document.createElement('div');
rulesContainer.style.display = 'flex';
rulesContainer.style.flexWrap = 'wrap';
rulesContainer.style.gap = '10px';
rulesContainer.style.alignItems = 'center';
rulesContainer.style.marginBottom = '10px';

// Current flags state for the checkboxes
const currentFlags = {
  ignoreCapital: false,
  startOfSentence: false,
  allInstances: false,
  preserveFirstCapital: false,
  global: false,
  noTrailingSpace: false,
  // New dialogue flags
  insideDialogueOnly: false,
  outsideDialogueOnly: false,
};

// Helper to create checkbox + label
function createCheckbox(flagKey, labelText) {
  const label = document.createElement('label');
  label.style.userSelect = 'none';
  label.style.fontSize = '13px';
  label.style.display = 'flex';
  label.style.alignItems = 'center';
  label.style.gap = '4px';
  label.style.whiteSpace = 'nowrap';
  label.style.flex = '0 1 auto'; // shrink if needed, don't force 100%

  const input = document.createElement('input');
  input.type = 'checkbox';
  input.checked = currentFlags[flagKey];
  input.style.cursor = 'pointer';

  input.addEventListener('change', () => {
    currentFlags[flagKey] = input.checked;
    // If global toggled, optionally do something like disable series input if you have one
  });

  label.appendChild(input);
  label.appendChild(document.createTextNode(labelText));
  return label;
}

rulesContainer.appendChild(createCheckbox('ignoreCapital', 'Ignore Capital'));
rulesContainer.appendChild(createCheckbox('startOfSentence', 'Start of Sentence'));
rulesContainer.appendChild(createCheckbox('allInstances', 'Fuzzy Match'));
rulesContainer.appendChild(createCheckbox('preserveFirstCapital', 'Preserve Capital'));
rulesContainer.appendChild(createCheckbox('global', 'Global'));
rulesContainer.appendChild(createCheckbox('noTrailingSpace', 'No Trailing Space'));
rulesContainer.appendChild(createCheckbox('insideDialogueOnly', 'Edit Inside Dialogue'));
rulesContainer.appendChild(createCheckbox('outsideDialogueOnly', 'Edit Outside Dialogue'));


popup.appendChild(rulesContainer);

  // Container that will hold search, filter, and list
  const listUIContainer = document.createElement('div');
  listUIContainer.style.display = 'none'; // hidden by default
  popup.appendChild(listUIContainer);

  // Search input
  const searchInput = document.createElement('input');
  searchInput.type = 'search';
  searchInput.placeholder = 'Search terms...';
  searchInput.style.width = '100%';
  searchInput.style.marginBottom = '10px';
  listUIContainer.appendChild(searchInput);

  // Filter select
  const toggleFilter = document.createElement('select');
  ['Current Series', 'Global + Others', 'All'].forEach(optText => {
    const option = document.createElement('option');
    option.textContent = optText;
    toggleFilter.appendChild(option);
  });
  toggleFilter.style.width = '100%';
  toggleFilter.style.marginBottom = '10px';
  listUIContainer.appendChild(toggleFilter);

  // Buttons container
  const btnContainer = document.createElement('div');
  btnContainer.style.marginBottom = '10px';
  btnContainer.style.textAlign = 'right';

  const exportBtn = document.createElement('button');
  exportBtn.textContent = 'Export';
  styleButton(exportBtn);
  btnContainer.appendChild(exportBtn);
  exportBtn.style.marginRight = '4px';
  exportBtn.style.padding = '2px 4px';
  

  const importBtn = document.createElement('button');
  importBtn.textContent = 'Import';
  styleButton(importBtn);
  importBtn.style.marginLeft = '4px';
  btnContainer.appendChild(importBtn);
  importBtn.style.padding = '2px 4px';

// --- Create current-series-only buttons ---
const exportCurrentBtn = document.createElement('button');
exportCurrentBtn.textContent = 'Export Current';
styleButton(exportCurrentBtn);
exportCurrentBtn.style.marginRight = '4px'; // spacing to the right
exportCurrentBtn.style.padding = '2px 4px';


const importCurrentBtn = document.createElement('button');
importCurrentBtn.textContent = 'Import Current';
styleButton(importCurrentBtn);
importCurrentBtn.style.marginRight = '4px'; // spacing to the right
importCurrentBtn.style.padding = '2px 4px';


// --- Append current-series buttons to the existing container ---
// Ensure they appear to the left of the original buttons
btnContainer.insertBefore(exportCurrentBtn, exportBtn);
btnContainer.insertBefore(importCurrentBtn, exportBtn);

// --- Event listeners for current-series buttons ---
exportCurrentBtn.addEventListener('click', function() {
  const seriesId = getCurrentSeriesId();
  if (!seriesId) {
    alert('No current series selected!');
    return;
  }

  // Only export entries whose series matches current series ID
  const exportData = [];
  for (const key in data) {
    (data[key] || []).forEach(entry => {
      if (entry.series === seriesId) {
        exportData.push(entry);
      }
    });
  }

  if (exportData.length === 0) {
    alert('No entries found for the current series.');
    return;
  }

  const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = `word-replacer-series-${seriesId}.json`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
});

importCurrentBtn.addEventListener('click', function() {
  const seriesId = getCurrentSeriesId();
  if (!seriesId) {
    alert('No current series selected!');
    return;
  }

  const seriesKey = `series-${seriesId}`;
  if (!data[seriesKey]) data[seriesKey] = [];

  const inputFile = document.createElement('input');
  inputFile.type = 'file';
  inputFile.accept = '.json,.txt';

  inputFile.addEventListener('change', (e) => {
    if (!e.target.files.length) return;
    const file = e.target.files[0];
    const reader = new FileReader();

    reader.onload = (event) => {
      try {
        const parsed = JSON.parse(event.target.result);
        if (!Array.isArray(parsed)) {
          alert('Invalid format: must be an array of replacement entries.');
          return;
        }

        // Assign all imported entries to current series
        parsed.forEach(entry => {
          if (!entry.from || !entry.to) return;
          data[seriesKey].push({
            ...entry,
            series: seriesId,   // force current series
            enabled: true,
          });
        });

        saveData(data);
        renderList();
        replaceTextInChapter();
      } catch (err) {
        alert('Import failed: ' + err.message);
      }
    };

    reader.readAsText(file);
  });

  inputFile.click();
});


  listUIContainer.appendChild(btnContainer);

  // List container
  const listContainer = document.createElement('div');
  listContainer.style.maxHeight = '260px';
  listContainer.style.overflowY = 'auto';
  listContainer.style.borderTop = '1px solid #ddd';
  listContainer.style.paddingTop = '8px';
  listUIContainer.appendChild(listContainer);

  // Style buttons helper
  function styleButton(btn) {
    btn.style.padding = '5px 12px';
    btn.style.fontSize = '13px';
    btn.style.cursor = 'pointer';
    btn.style.border = '1px solid #888';
    btn.style.borderRadius = '4px';
    btn.style.backgroundColor = '#eee';
    btn.style.userSelect = 'none';
  }
toggleListBtn.addEventListener('click', () => {
  const isShowing = listUIContainer.style.display !== 'none';

  if (!isShowing) {
    // Show the list, hide the rules
    listUIContainer.style.display = 'block';
    rulesContainer.style.display = 'none';
    toggleListBtn.textContent = 'Hide List';

    renderList(); // refresh list when showing
  } else {
    // Hide the list, show the rules
    listUIContainer.style.display = 'none';
    rulesContainer.style.display = 'flex';
    toggleListBtn.textContent = 'Show List';
  }
});


  // Get current series id helper
function getCurrentSeriesId() {
  // Match new URL structure: /novel/{id}/
  const urlMatch = location.href.match(/\/novel\/(\d+)\//i);
  if (urlMatch) return urlMatch[1];

  // Fallback: check breadcrumb links
  const crumb = document.querySelector('.breadcrumb li.breadcrumb-item a[href*="/novel/"]');
  if (crumb) {
    const crumbMatch = crumb.href.match(/\/novel\/(\d+)\//i);
    if (crumbMatch) return crumbMatch[1];
  }

  return null;
}


  // Render list of replacements
  function renderList() {
    listContainer.innerHTML = '';

    const seriesId = getCurrentSeriesId();

    let keysToShow = [];

    if (toggleFilter.value === 'Current Series') {
      if (seriesId) keysToShow = [`series-${seriesId}`];
      else keysToShow = [];
    } else if (toggleFilter.value === 'Global + Others') {
      keysToShow = Object.keys(data).filter(k => k !== `series-${seriesId}`);
    } else { // All
      keysToShow = Object.keys(data);
    }

    let allEntries = [];
    keysToShow.forEach(key => {
      if (data[key]) allEntries = allEntries.concat(data[key]);
    });

    const searchLower = searchInput.value.trim().toLowerCase();
if (searchLower) {
  allEntries = allEntries.filter(e =>
    (e.from && e.from.toLowerCase().includes(searchLower)) ||
    (e.to && e.to.toLowerCase().includes(searchLower))
  );
}

    if (allEntries.length === 0) {
      const emptyMsg = document.createElement('div');
      emptyMsg.textContent = 'No terms found.';
      emptyMsg.style.fontStyle = 'italic';
      listContainer.appendChild(emptyMsg);
      return;
    }

allEntries.forEach((entry) => {
  const row = document.createElement('div');
  row.style.display = 'flex';
  row.style.alignItems = 'flex-start'; // align top when text wraps
  row.style.justifyContent = 'space-between'; // space between left text and right controls
  row.style.marginBottom = '6px';
  row.style.width = '100%';

  // ---- Left side (text) ----
  const textContainer = document.createElement('div');
  textContainer.style.display = 'flex';
  textContainer.style.flexDirection = 'row';
  textContainer.style.flexWrap = 'wrap';
  textContainer.style.flexGrow = '1';
  textContainer.style.minWidth = '0'; // needed for wrapping

  const fromSpan = document.createElement('span');
  fromSpan.textContent = entry.from;
  fromSpan.style.cursor = 'pointer';
  fromSpan.style.userSelect = 'none';
  fromSpan.style.color = '#007bff';
  fromSpan.style.wordBreak = 'break-word';
  fromSpan.style.overflowWrap = 'anywhere';
  fromSpan.addEventListener('click', () => {
    openEditDialog(entry);
  });

  const toSpan = document.createElement('span');
  toSpan.textContent = ' → ' + entry.to;
  toSpan.style.marginLeft = '8px';
  toSpan.style.wordBreak = 'break-word';
  toSpan.style.overflowWrap = 'anywhere';

  textContainer.appendChild(fromSpan);
  textContainer.appendChild(toSpan);

  // ---- Right side (controls) ----
  const controls = document.createElement('div');
  controls.style.display = 'flex';
  controls.style.alignItems = 'center';
  controls.style.flexShrink = '0'; // don’t shrink controls
  controls.style.marginLeft = '12px';

  const enabledCheckbox = document.createElement('input');
  enabledCheckbox.type = 'checkbox';
  enabledCheckbox.checked = entry.enabled ?? true;
  enabledCheckbox.title = 'Enable / Disable this replacement';
  enabledCheckbox.style.marginRight = '8px';
  enabledCheckbox.addEventListener('change', () => {
    entry.enabled = enabledCheckbox.checked;
    saveData(data);
    replaceTextInChapter();
  });

  const deleteBtn = document.createElement('button');
  deleteBtn.textContent = '✕';
  styleButton(deleteBtn);
  deleteBtn.title = 'Delete this replacement';
  deleteBtn.addEventListener('click', () => {
    deleteEntry(entry);
  });

  controls.appendChild(enabledCheckbox);
  controls.appendChild(deleteBtn);

  // ---- Combine ----
  row.appendChild(textContainer);
  row.appendChild(controls);

  listContainer.appendChild(row);
});
  }

    // Delete entry
    function deleteEntry(entry) {
      for (const key in data) {
        const arr = data[key];
        const idx = arr.findIndex(e => e.from === entry.from);
        if (idx >= 0) {
          arr.splice(idx, 1);
          if (arr.length === 0 && key !== 'global') {
            delete data[key];
          }
          saveData(data);
          renderList();
          replaceTextInChapter();
          break;
        }
      }
    }

    // Edit dialog popup
    function openEditDialog(entry) {
      const modalBg = document.createElement('div');
      Object.assign(modalBg.style, {
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: 'rgba(0,0,0,0.5)',
        zIndex: 100001,
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      });

const modal = document.createElement('div');
Object.assign(modal.style, {
  backgroundColor: isInverted ? '#000' : 'white',
  color: isInverted ? '#fff' : '#000',
  padding: '20px',
  borderRadius: '8px',
  width: '320px',
  boxShadow: '0 0 15px rgba(0,0,0,0.3)',
  fontSize: '14px',
});

      modalBg.appendChild(modal);

      // Title
      const title = document.createElement('h3');
      title.textContent = 'Edit Replacement';
      title.style.marginTop = '0';
      modal.appendChild(title);

      // From input
      const fromLabel = document.createElement('label');
      fromLabel.textContent = 'Find: ';
      const fromInput = document.createElement('input');
      fromInput.type = 'text';
      fromInput.value = entry.from;
      fromInput.style.width = '100%';
      fromInput.required = true;
      fromLabel.appendChild(fromInput);
      modal.appendChild(fromLabel);

      modal.appendChild(document.createElement('br'));

      // To input
      const toLabel = document.createElement('label');
      toLabel.textContent = 'Replace with: ';
      const toInput = document.createElement('input');
      toInput.type = 'text';
      toInput.value = entry.to;
      toInput.style.width = '100%';
      toLabel.appendChild(toInput);
      modal.appendChild(toLabel);

      modal.appendChild(document.createElement('br'));
	  
	  const noteBtn = document.createElement('button');
      noteBtn.textContent = entry.note ? 'Edit Note' : 'Add Note';
      noteBtn.style.marginTop = '8px';
      noteBtn.style.display = 'block';

      modal.appendChild(noteBtn);
noteBtn.addEventListener('click', () => openNoteModal(entry, noteBtn));


function openNoteModal(entry, buttonRef) {
  const noteModalBg = document.createElement('div');
  Object.assign(noteModalBg.style, {
    position: 'fixed',
    top: 0,
    left: 0,
    width: '100vw',
    height: '100vh',
    backgroundColor: 'rgba(0,0,0,0.4)',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 999999,
  });

  const noteModal = document.createElement('div');
  Object.assign(noteModal.style, {
    backgroundColor: 'black',
    color: 'black',
    padding: '20px',
    borderRadius: '8px',
    width: '280px',
    boxShadow: '0 0 15px rgba(0,0,0,0.3)',
    fontSize: '14px',
  });

  noteModalBg.appendChild(noteModal);

  const noteTitle = document.createElement('h3');
  noteTitle.textContent = 'Add Note';
  noteTitle.style.marginTop = '0';
  noteModal.appendChild(noteTitle);
  noteTitle.style.color = 'white';

  const noteInput = document.createElement('textarea');
  noteInput.rows = 3;
  noteInput.maxLength = 30;
  noteInput.value = entry.note || '';
  noteInput.style.width = '100%';
  noteInput.placeholder = 'Enter a short note';
  noteModal.appendChild(noteInput);

  const noteSave = document.createElement('button');
  noteSave.textContent = 'Save';
  noteSave.style.marginRight = '10px';

  const noteCancel = document.createElement('button');
  noteCancel.textContent = 'Cancel';

  noteModal.appendChild(noteSave);
  noteModal.appendChild(noteCancel);

  document.body.appendChild(noteModalBg);

  noteSave.addEventListener('click', () => {
    entry.note = noteInput.value.trim().slice(0, 30);
    if (buttonRef) buttonRef.textContent = entry.note ? 'Edit Note' : 'Add Note';
    document.body.removeChild(noteModalBg);
  });

  noteCancel.addEventListener('click', () => {
    document.body.removeChild(noteModalBg);
  });
}




      // Enabled checkbox
      const enabledLabel = document.createElement('label');
      const enabledInput = document.createElement('input');
      enabledInput.type = 'checkbox';
      enabledInput.checked = entry.enabled ?? true;
      enabledLabel.appendChild(enabledInput);
      enabledLabel.append(' Enabled');
      enabledLabel.style.userSelect = 'none';
      modal.appendChild(enabledLabel);

      modal.appendChild(document.createElement('br'));

      // Flags container
      const flags = [
        { key: 'ignoreCapital', label: 'Ignore Capitalization' },
        { key: 'startOfSentence', label: 'Match Whether Start of Sentence' },
        { key: 'allInstances', label: 'Fuzzy Match' },
        { key: 'preserveFirstCapital', label: 'Preserve First Capital Letter' },
        { key: 'noTrailingSpace', label: 'No Trailing Space' },
        { key: 'insideDialogueOnly', label: 'Edit Only Inside Dialogue' },
        { key: 'outsideDialogueOnly', label: 'Edit Only Outside Dialogue' },
      ];

      flags.forEach(f => {
        const flagLabel = document.createElement('label');
        const flagInput = document.createElement('input');
        flagInput.type = 'checkbox';
        flagInput.checked = entry[f.key] ?? false;
        flagLabel.appendChild(flagInput);
        flagLabel.append(' ' + f.label);
        flagLabel.style.display = 'block';
        flagLabel.style.userSelect = 'none';
        modal.appendChild(flagLabel);

        flagInput.addEventListener('change', () => {
          entry[f.key] = flagInput.checked;
        });
      });

      modal.appendChild(document.createElement('br'));

      // Series input (optional)
      const seriesLabel = document.createElement('label');
      seriesLabel.textContent = 'Series ID (empty = global): ';
      const seriesInput = document.createElement('input');
      seriesInput.type = 'text';
      seriesInput.value = entry.series || '';
      seriesInput.style.width = '100%';
      seriesLabel.appendChild(seriesInput);
      modal.appendChild(seriesLabel);

      modal.appendChild(document.createElement('br'));

      // Buttons
      const btnSave = document.createElement('button');
      btnSave.textContent = 'Save';
      btnSave.style.marginRight = '10px';

      const btnCancel = document.createElement('button');
      btnCancel.textContent = 'Cancel';

      modal.appendChild(btnSave);
      modal.appendChild(btnCancel);

      // Save handler
btnSave.addEventListener('click', () => {
  let f = fromInput.value;
  const t = toInput.value;



  // Only trim the search word if this entry has noTrailingSpace enabled
  if (entry.noTrailingSpace) {
    f = f.trim();
  }
        // Update entry properties
        // If series changed, move to correct group
        const oldSeriesKey = entry.series ? `series-${entry.series}` : 'global';
        const newSeriesKey = seriesInput.value ? `series-${seriesInput.value}` : 'global';

        if (oldSeriesKey !== newSeriesKey) {
          // Remove from old array
          if (data[oldSeriesKey]) {
            const idx = data[oldSeriesKey].indexOf(entry);
            if (idx >= 0) data[oldSeriesKey].splice(idx, 1);
            if (data[oldSeriesKey].length === 0 && oldSeriesKey !== 'global') {
              delete data[oldSeriesKey];
            }
          }
          // Add to new array
          if (!data[newSeriesKey]) data[newSeriesKey] = [];
          data[newSeriesKey].push(entry);
          entry.series = seriesInput.value.trim();
        }

        entry.from = f;
        entry.to = t;
        entry.enabled = enabledInput.checked;

        saveData(data);
        renderList();
        replaceTextInChapter();
        closeEditModal();
      });

      btnCancel.addEventListener('click', () => {
        closeEditModal();
      });

      function closeEditModal() {
        modalBg.remove();
      }

      document.body.appendChild(modalBg);
    }

    // Add new entry controls at bottom
    const addNewLabel = document.createElement('div');
    addNewLabel.textContent = 'Add New Replacement:';
    addNewLabel.style.marginTop = '15px';
    addNewLabel.style.fontWeight = 'bold';
    popup.appendChild(addNewLabel);

const inputContainer = document.createElement('div');
inputContainer.style.display = 'flex';
inputContainer.style.gap = '6px';
inputContainer.style.marginTop = '6px';
inputContainer.style.flexWrap = 'nowrap';  // keep inputs + button on one line
inputContainer.style.alignItems = 'center'; // vertical alignment

const fromInputNew = document.createElement('input');
fromInputNew.placeholder = 'Find';
fromInputNew.style.flex = '1';   // take available width
fromInputNew.style.minWidth = '60px'; // prevent shrinking too much
inputContainer.appendChild(fromInputNew);
const toInputNew = document.createElement('input');
toInputNew.placeholder = 'Replace with';
toInputNew.style.flex = '1';
toInputNew.style.minWidth = '60px';
inputContainer.appendChild(toInputNew);
    // Autocomplete suggestion box for "Replace with"
const replaceSuggestionBox = document.createElement('ul');
Object.assign(replaceSuggestionBox.style, {
  position: 'absolute',
  zIndex: 9999,
  border: '1px solid #ccc',
  background: '#000',     // solid black background
  color: '#fff',          // white text
  listStyle: 'none',
  margin: 0,
  padding: 0,
  maxHeight: '120px',
  overflowY: 'auto',
  display: 'none',
  opacity: '1',           // fully opaque
});
inputContainer.appendChild(replaceSuggestionBox);

// Helper to position suggestion box above the input
function positionReplaceBox() {
  // Make it visible temporarily to measure its height
  replaceSuggestionBox.style.display = 'block';

  const rect = toInputNew.getBoundingClientRect();
  const containerRect = inputContainer.getBoundingClientRect();

  // Place the bottom of the box at the top of the input
  replaceSuggestionBox.style.left = (toInputNew.offsetLeft) + 'px';
  replaceSuggestionBox.style.top = (toInputNew.offsetTop - replaceSuggestionBox.offsetHeight) + 'px';
}


// Input listener
toInputNew.addEventListener('input', () => {
  const val = toInputNew.value.trim().toLowerCase();
  replaceSuggestionBox.innerHTML = '';

  if (val.length < 2) {
    replaceSuggestionBox.style.display = 'none';
    return;
  }

  // Pull all "to" values from data across all series + global
  const allTerms = Object.values(data)
    .flat()
    .map(entry => entry.to)
    .filter((v, i, self) => v && self.indexOf(v) === i); // unique & non-empty

  const matches = allTerms.filter(term => term.toLowerCase().includes(val));

  if (!matches.length) {
    replaceSuggestionBox.style.display = 'none';
    return;
  }

matches.forEach(term => {
  const li = document.createElement('li');
  li.textContent = term;
  li.style.padding = '4px 6px';
  li.style.cursor = 'pointer';
  li.style.background = '#000';  // default black background
  li.style.color = '#fff';       // default white text

  li.addEventListener('mousedown', (e) => {
    e.preventDefault(); // prevent blur
    toInputNew.value = term;
    replaceSuggestionBox.style.display = 'none';
  });

  // Remove the previous hover effects
  li.addEventListener('mouseover', () => {
    li.style.background = '#111'; // slightly lighter black on hover
  });
  li.addEventListener('mouseout', () => {
    li.style.background = '#000'; // back to solid black
  });

  replaceSuggestionBox.appendChild(li);
});


  positionReplaceBox();
  replaceSuggestionBox.style.display = 'block';
});

// Hide suggestions when clicking outside
document.addEventListener('click', (e) => {
  if (!inputContainer.contains(e.target)) {
    replaceSuggestionBox.style.display = 'none';
  }
});


    const addBtn = document.createElement('button');
    addBtn.textContent = 'Add';
    styleButton(addBtn);

addBtn.addEventListener('click', () => {
let f = fromInputNew.value;
const t = toInputNew.value;

// Check the actual checkbox state at creation time
const noTrailingSpaceChecked = document.querySelector('#noTrailingSpaceCheckboxId')?.checked;
if (noTrailingSpaceChecked) {
  f = f.trim();
}

  if (!f) {
    alert('Find term cannot be empty');
    return;
  }

  // Use global checkbox to decide seriesId: empty means global
  const seriesId = currentFlags.global ? '' : getCurrentSeriesId();
  const seriesKey = seriesId ? `series-${seriesId}` : 'global';

  if (!data[seriesKey]) data[seriesKey] = [];

  // Avoid duplicates in that series/global bucket
  if (data[seriesKey].some(e => e.from.toLowerCase() === f.toLowerCase())) {
    alert('This find term already exists in this series/global.');
    return;
  }

data[seriesKey].push({
  from: f,
  to: t,
  note: '', // <-- new
  enabled: true,
  ignoreCapital: currentFlags.ignoreCapital,
  startOfSentence: currentFlags.startOfSentence,
  allInstances: currentFlags.allInstances,
  preserveFirstCapital: currentFlags.preserveFirstCapital,
  series: seriesId || '',
  noTrailingSpace: currentFlags.noTrailingSpace,
  insideDialogueOnly: currentFlags.insideDialogueOnly,
  outsideDialogueOnly: currentFlags.outsideDialogueOnly,
});

  saveData(data);
  fromInputNew.value = '';
  toInputNew.value = '';
  renderList();
  replaceTextInChapter();
});


    inputContainer.appendChild(fromInputNew);
    inputContainer.appendChild(toInputNew);
    inputContainer.appendChild(addBtn);
    popup.appendChild(inputContainer);

    // Export button
    exportBtn.addEventListener('click', () => {
      const dataStr = JSON.stringify(data, null, 2);
      const blob = new Blob([dataStr], { type: 'application/json' });
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = 'word-replacer-data.json';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);

      URL.revokeObjectURL(url);
    });

    // Import button
    importBtn.addEventListener('click', () => {
      const inputFile = document.createElement('input');
      inputFile.type = 'file';
      inputFile.accept = '.json,.txt';

      inputFile.addEventListener('change', (e) => {
        if (!e.target.files.length) return;
        const file = e.target.files[0];
        const reader = new FileReader();

function importData(parsed) {
  if (typeof parsed === 'object' && !Array.isArray(parsed)) {
    // Assume full structured data (keys like 'global', 'series-...')
    // Merge imported data with existing data (append pairs)
    for (const key in parsed) {
      if (!data[key]) data[key] = [];
      // Append new entries (avoid duplicates if needed)
      parsed[key].forEach(newEntry => data[key].push(newEntry));
    }
  } else if (Array.isArray(parsed)) {
    // Array of pairs (old simple format)
    if (!data.global) data.global = [];
    const newPairs = parsed.map(pair => {
      if (!Array.isArray(pair) || pair.length < 2) return null;
      return {
        from: pair[0],
        to: pair[1],
        enabled: true,
        startOfSentence: false,
        ignoreCapital: false,
        allInstances: false,
        preserveFirstCapital: false,
        global: true,
        seriesId: ''
      };
    }).filter(Boolean);
    data.global.push(...newPairs);
  } else {
    alert('Import failed: unsupported format.');
    return;
  }
  saveData(data);
  renderList();
  replaceTextInChapter();
}

reader.onload = (e) => {
  try {
    const parsed = JSON.parse(e.target.result);
    importData(parsed);
    alert('Import successful!');
  } catch (err) {
    alert('Invalid JSON: ' + err.message);
  }
};
        reader.readAsText(file);
      });

      inputFile.click();
    });

    // Search and filter event handlers
    searchInput.addEventListener('input', renderList);
    toggleFilter.addEventListener('change', renderList);

    renderList();

    document.body.appendChild(popup);
  }

  // Run once immediately
  startReplaceLoop();

})();