240 lines
6.5 KiB
JavaScript
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;
|
|
};
|