// Text selection functionality window.textRemover = new (function () { let selectionUI = null; let isUIActive = false; function createSelectionUI() { if (selectionUI) return selectionUI; const { div, button } = van.tags; selectionUI = div( { id: 'text-selection-ui', role: 'group', }, button( { type: 'button', onclick: e => { if (reader.hidden.val) { e.stopPropagation(); } removeSelectedText(); }, }, reader.strings.removeText, ), button( { type: 'button', onclick: e => { if (reader.hidden.val) { e.stopPropagation(); } replaceSelectedText(); }, }, reader.strings.replaceText, ), ); document.body.appendChild(selectionUI); return selectionUI; } function showSelectionUI() { const ui = createSelectionUI(); // Get selection bounds const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // Get UI element heights from CSS variables (with fallbacks) const statusBarHeight = parseInt( getComputedStyle(document.documentElement).getPropertyValue( '--StatusBar-currentHeight', ), 10, ) || 24; const navigationBarHeight = parseInt( getComputedStyle(document.documentElement).getPropertyValue( '--bottom-inset', ), 10, ) || 24; const readerPadding = parseInt( getComputedStyle(document.documentElement).getPropertyValue( '--readerSettings-padding', ), 10, ) || 16; const uiHeight = 50; // Approximate height of our UI // Calculate available space const viewportHeight = window.innerHeight; const selectionCenterY = rect.top + rect.height / 2; const topSafeArea = statusBarHeight + readerPadding + 10; const bottomSafeArea = readerPadding + uiHeight + navigationBarHeight; // Position UI based on selection location let topPosition; if (selectionCenterY < (viewportHeight / 5) * 4) { // Selection is in top 4/5, position UI at bottom //TODO: make this dynamic const avoidScrollbar = reader.generalSettings.val.verticalSeekbar ? 0 : 42; const avoidUI = !reader.hidden.val ? 46 + avoidScrollbar : 0; topPosition = viewportHeight - bottomSafeArea - avoidUI - 4; ui.style.top = topPosition + 'px'; ui.style.bottom = 'auto'; } else { // Selection is in bottom 1/5, position UI at top (accounting for status bar) topPosition = Math.max(topSafeArea, statusBarHeight + 20); const avoidUI = !reader.hidden.val ? 34 : 0; ui.style.top = topPosition + avoidUI + 'px'; ui.style.bottom = 'auto'; } // Center horizontally ui.style.left = '50%'; ui.style.transform = 'translateX(-50%)'; } else { // Fallback: position at top if no selection rect available ui.style.top = '20px'; ui.style.left = '50%'; ui.style.transform = 'translateX(-50%)'; ui.style.bottom = 'auto'; } ui.style.opacity = '1'; ui.style.pointerEvents = 'auto'; isUIActive = true; } function hideSelectionUI() { if (selectionUI) { selectionUI.style.opacity = '0'; selectionUI.style.pointerEvents = 'none'; } isUIActive = false; } function getSelectedText() { const selection = window.getSelection(); if (selection.rangeCount > 0) { return selection.toString().trim(); } return ''; } function removeSelectedText() { const selectedText = getSelectedText(); if (selectedText) { reader.post({ type: 'text-action', data: { remove: selectedText }, }); } hideSelectionUI(); window.getSelection().removeAllRanges(); } function replaceSelectedText() { const selectedText = getSelectedText(); if (selectedText) { // For replace, we need user input, so send a different message reader.post({ type: 'text-action', data: { replace: selectedText }, }); } hideSelectionUI(); window.getSelection().removeAllRanges(); } // Handle text selection document.addEventListener('selectionchange', function () { const selectedText = getSelectedText(); if (selectedText) { showSelectionUI(); } else if (!isUIActive) { hideSelectionUI(); } }); // Hide UI when clicking/tapping elsewhere document.addEventListener('touchstart', function (e) { if (isUIActive && selectionUI && !selectionUI.contains(e.target)) { const selectedText = getSelectedText(); if (!selectedText) { hideSelectionUI(); } } }); document.addEventListener('click', function (e) { if (isUIActive && selectionUI && !selectionUI.contains(e.target)) { const selectedText = getSelectedText(); if (!selectedText) { hideSelectionUI(); } } }); // Hide UI on scroll window.addEventListener('scroll', function () { hideSelectionUI(); }); })(); /** * Directly remove text from the chapter DOM without reloading the WebView. * Also updates reader.rawHTML so the text-options deriver (bionic reading, * paragraph spacing) doesn't re-introduce removed text on its next run. */ window.textRemover.performRemove = function (text) { const el = document.querySelector('#LNReader-chapter'); if (!el) return; const m = text.match(/^\/(.*)\/([gmiyuvsd]*)$/); let result; if (m) { try { result = el.innerHTML.replace(new RegExp(m[1], m[2]), ''); } catch (_e) { return; } } else { result = el.innerHTML.split(text).join(''); } el.innerHTML = result; reader.rawHTML = result; }; /** * Directly replace text in the chapter DOM without reloading the WebView. * Also updates reader.rawHTML so the text-options deriver stays in sync. */ window.textRemover.performReplace = function (from, to) { const el = document.querySelector('#LNReader-chapter'); if (!el) return; const m = from.match(/^\/(.*)\/([gmiyuvsd]*)$/); let result; if (m) { try { result = el.innerHTML.replace(new RegExp(m[1], m[2]), to); } catch (_e) { return; } } else { result = el.innerHTML.split(from).join(to); } el.innerHTML = result; reader.rawHTML = result; };