Files
2026-09-23 10:41:38 +05:30

240 lines
6.5 KiB
JavaScript

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