pake-cli
Version:
🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。
709 lines (618 loc) • 17.3 kB
JavaScript
(function () {
if (window.__PAKE_FIND_SCRIPT__) {
return;
}
window.__PAKE_FIND_SCRIPT__ = true;
const PANEL_ID = "pake-find-panel";
const STYLE_ID = "pake-find-style";
const MARK_ATTR = "data-pake-find";
const ACTIVE_ATTR = "data-pake-find-active";
const MATCH_HIGHLIGHT = "pake-find-match";
const ACTIVE_HIGHLIGHT = "pake-find-active";
const MAX_MATCHES = 1000;
const SEARCH_DEBOUNCE_MS = 120;
const SKIPPED_TAGS = new Set([
"script",
"style",
"noscript",
"input",
"textarea",
"select",
"option",
]);
const state = {
enabled: window.pakeConfig?.enable_find === true,
panel: null,
input: null,
counter: null,
status: null,
matches: [],
activeIndex: -1,
query: "",
truncated: false,
domMarks: [],
observer: null,
searchTimer: null,
isOpen: false,
};
function getState() {
return {
enabled: state.enabled,
isOpen: state.isOpen,
query: state.query,
matchCount: state.matches.length,
activeIndex: state.activeIndex,
truncated: state.truncated,
};
}
function noop() {
return getState();
}
if (!state.enabled) {
window.pakeFind = {
open: noop,
close: noop,
next: noop,
previous: noop,
search: noop,
getState,
getFindShortcutAction: () => "",
};
return;
}
function getNodeFilter() {
return (
window.NodeFilter ||
globalThis.NodeFilter || {
SHOW_TEXT: 4,
FILTER_ACCEPT: 1,
FILTER_REJECT: 2,
}
);
}
function supportsCustomHighlight() {
return (
typeof CSS !== "undefined" &&
CSS.highlights &&
typeof Highlight === "function"
);
}
function isFindPanelNode(node) {
const element =
node?.nodeType === 1 ? node : node?.parentElement || node?.parentNode;
if (!element) {
return false;
}
if (element.id === PANEL_ID) {
return true;
}
return element.closest?.(`#${PANEL_ID}`) != null;
}
function shouldSkipElement(element) {
for (let current = element; current; current = current.parentElement) {
if (current.id === PANEL_ID) {
return true;
}
const tagName = current.tagName?.toLowerCase();
if (tagName && SKIPPED_TAGS.has(tagName)) {
return true;
}
if (
current.isContentEditable ||
current.getAttribute?.("contenteditable") === "true"
) {
return true;
}
if (current.hidden || current.getAttribute?.("aria-hidden") === "true") {
return true;
}
}
return false;
}
function getSearchableTextNodes(root = document.body) {
if (!root || !document.createTreeWalker) {
return [];
}
const nodeFilter = getNodeFilter();
const walker = document.createTreeWalker(root, nodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (!node.nodeValue || node.nodeValue.length === 0) {
return nodeFilter.FILTER_REJECT;
}
if (shouldSkipElement(node.parentElement)) {
return nodeFilter.FILTER_REJECT;
}
return nodeFilter.FILTER_ACCEPT;
},
});
const nodes = [];
let current = walker.nextNode();
while (current) {
nodes.push(current);
current = walker.nextNode();
}
return nodes;
}
function createRange(node, start, end) {
const range = document.createRange();
range.setStart(node, start);
range.setEnd(node, end);
return range;
}
function collectMatches(query) {
const matches = [];
const normalizedQuery = query.toLocaleLowerCase();
if (!normalizedQuery) {
return { matches, truncated: false };
}
for (const node of getSearchableTextNodes()) {
const text = node.nodeValue || "";
const normalizedText = text.toLocaleLowerCase();
let searchFrom = 0;
while (searchFrom <= normalizedText.length) {
const index = normalizedText.indexOf(normalizedQuery, searchFrom);
if (index === -1) {
break;
}
matches.push({
node,
start: index,
end: index + query.length,
range: createRange(node, index, index + query.length),
mark: null,
});
if (matches.length >= MAX_MATCHES) {
return { matches, truncated: true };
}
searchFrom = index + Math.max(query.length, 1);
}
}
return { matches, truncated: false };
}
function ensureStyle() {
if (document.getElementById(STYLE_ID)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${PANEL_ID} {
position: fixed;
top: 14px;
right: 14px;
z-index: 2147483647;
display: none;
align-items: center;
gap: 6px;
box-sizing: border-box;
min-width: 278px;
max-width: min(420px, calc(100vw - 28px));
padding: 8px;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
color: #1f2328;
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
backdrop-filter: blur(16px);
}
#${PANEL_ID}[data-visible="true"] {
display: flex;
}
#${PANEL_ID} input {
min-width: 0;
flex: 1 1 auto;
height: 28px;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.16);
border-radius: 6px;
padding: 0 8px;
background: #fff;
color: #1f2328;
font: inherit;
outline: none;
}
#${PANEL_ID} input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.16);
}
#${PANEL_ID} [data-pake-find-counter] {
flex: 0 0 auto;
min-width: 42px;
color: #5f6b7a;
text-align: center;
font-size: 12px;
white-space: nowrap;
}
#${PANEL_ID} button {
flex: 0 0 auto;
width: 28px;
height: 28px;
border: 0;
border-radius: 6px;
background: transparent;
color: #30363d;
font: 15px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
cursor: pointer;
}
#${PANEL_ID} button:hover {
background: rgba(0, 0, 0, 0.08);
}
#${PANEL_ID} [data-pake-find-status] {
position: absolute;
left: 10px;
top: calc(100% + 4px);
color: #d1242f;
font-size: 12px;
white-space: nowrap;
}
@media (prefers-color-scheme: dark) {
#${PANEL_ID} {
border-color: rgba(255, 255, 255, 0.16);
background: rgba(31, 35, 40, 0.94);
color: #f0f3f6;
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.36);
}
#${PANEL_ID} input {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.08);
color: #f0f3f6;
}
#${PANEL_ID} [data-pake-find-counter] {
color: #b7c0cc;
}
#${PANEL_ID} button {
color: #f0f3f6;
}
#${PANEL_ID} button:hover {
background: rgba(255, 255, 255, 0.12);
}
}
::highlight(${MATCH_HIGHLIGHT}) {
background: rgba(255, 214, 10, 0.58);
color: inherit;
}
::highlight(${ACTIVE_HIGHLIGHT}) {
background: rgba(255, 149, 0, 0.9);
color: inherit;
}
mark[${MARK_ATTR}] {
background: rgba(255, 214, 10, 0.58);
color: inherit;
padding: 0;
}
mark[${MARK_ATTR}][${ACTIVE_ATTR}] {
background: rgba(255, 149, 0, 0.9);
}
`;
(document.head || document.body || document.documentElement)?.appendChild(
style,
);
}
function createButton(label, title, onClick) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.title = title;
button.setAttribute("aria-label", title);
button.addEventListener("click", onClick);
return button;
}
function ensurePanel() {
if (state.panel) {
return state.panel;
}
ensureStyle();
const panel = document.createElement("div");
panel.id = PANEL_ID;
panel.setAttribute("role", "search");
panel.setAttribute("aria-label", "Find in page");
const input = document.createElement("input");
input.type = "search";
input.autocomplete = "off";
input.spellcheck = false;
input.placeholder = "Find";
input.setAttribute("aria-label", "Find in page");
const counter = document.createElement("span");
counter.setAttribute("data-pake-find-counter", "");
counter.textContent = "0/0";
const previousButton = createButton("<", "Find Previous", () => previous());
const nextButton = createButton(">", "Find Next", () => next());
const closeButton = createButton("x", "Close Find", () => close());
const status = document.createElement("span");
status.setAttribute("data-pake-find-status", "");
input.addEventListener("input", () => {
debounceSearch(input.value);
});
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
if (event.shiftKey) {
previous();
} else {
next();
}
return;
}
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
close();
}
});
panel.append(
input,
counter,
previousButton,
nextButton,
closeButton,
status,
);
(document.body || document.documentElement).appendChild(panel);
state.panel = panel;
state.input = input;
state.counter = counter;
state.status = status;
return panel;
}
function clearCustomHighlights() {
if (!supportsCustomHighlight()) {
return;
}
CSS.highlights.delete(MATCH_HIGHLIGHT);
CSS.highlights.delete(ACTIVE_HIGHLIGHT);
}
function clearDomMarks() {
const marks = Array.from(
document.querySelectorAll?.(`mark[${MARK_ATTR}]`) || state.domMarks,
);
for (const mark of marks) {
const parent = mark.parentNode;
const text = document.createTextNode(mark.textContent || "");
mark.replaceWith?.(text);
parent?.normalize?.();
}
state.domMarks = [];
}
function clearHighlights() {
clearCustomHighlights();
clearDomMarks();
}
function applyCustomHighlights() {
if (!supportsCustomHighlight()) {
return false;
}
const ranges = state.matches.map((match) => match.range);
CSS.highlights.set(MATCH_HIGHLIGHT, new Highlight(...ranges));
updateActiveHighlight();
return true;
}
function applyDomHighlights() {
const grouped = new Map();
for (const match of state.matches) {
const nodeMatches = grouped.get(match.node) || [];
nodeMatches.push(match);
grouped.set(match.node, nodeMatches);
}
for (const nodeMatches of grouped.values()) {
nodeMatches.sort((a, b) => b.start - a.start);
for (const match of nodeMatches) {
try {
const mark = document.createElement("mark");
mark.setAttribute(MARK_ATTR, "");
match.range.surroundContents(mark);
match.mark = mark;
state.domMarks.push(mark);
} catch (error) {
// Some browser-generated text ranges cannot be wrapped safely.
}
}
}
updateDomActiveMark();
}
function updateDomActiveMark() {
state.matches.forEach((match, index) => {
const mark = match.mark;
if (!mark) {
return;
}
if (mark.toggleAttribute) {
mark.toggleAttribute(ACTIVE_ATTR, index === state.activeIndex);
} else if (index === state.activeIndex) {
mark.setAttribute(ACTIVE_ATTR, "");
} else {
mark.removeAttribute?.(ACTIVE_ATTR);
}
});
}
function updateActiveHighlight() {
if (!supportsCustomHighlight()) {
updateDomActiveMark();
return;
}
CSS.highlights.delete(ACTIVE_HIGHLIGHT);
if (state.activeIndex >= 0 && state.matches[state.activeIndex]) {
CSS.highlights.set(
ACTIVE_HIGHLIGHT,
new Highlight(state.matches[state.activeIndex].range),
);
}
}
function scrollActiveIntoView() {
const active = state.matches[state.activeIndex];
if (!active) {
return;
}
const target = active.mark || active.range.startContainer?.parentElement;
if (target?.scrollIntoView) {
target.scrollIntoView({ block: "center", inline: "nearest" });
}
}
function updateCounter() {
if (!state.counter) {
return;
}
const total = state.matches.length;
const active = state.activeIndex >= 0 ? state.activeIndex + 1 : 0;
state.counter.textContent = `${active}/${total}${state.truncated ? "+" : ""}`;
if (state.status) {
state.status.textContent = state.query && total === 0 ? "No results" : "";
}
}
function runSearch(query = state.query) {
state.query = query;
clearHighlights();
if (!query) {
state.matches = [];
state.activeIndex = -1;
state.truncated = false;
updateCounter();
return getState();
}
const result = collectMatches(query);
state.matches = result.matches;
state.truncated = result.truncated;
state.activeIndex = state.matches.length > 0 ? 0 : -1;
if (!applyCustomHighlights()) {
applyDomHighlights();
}
updateCounter();
scrollActiveIntoView();
return getState();
}
function debounceSearch(query) {
clearTimeout(state.searchTimer);
state.searchTimer = setTimeout(() => runSearch(query), SEARCH_DEBOUNCE_MS);
}
function next() {
if (!state.query && state.input?.value) {
runSearch(state.input.value);
}
if (state.matches.length === 0) {
return getState();
}
state.activeIndex = (state.activeIndex + 1) % state.matches.length;
updateActiveHighlight();
updateCounter();
scrollActiveIntoView();
return getState();
}
function previous() {
if (!state.query && state.input?.value) {
runSearch(state.input.value);
}
if (state.matches.length === 0) {
return getState();
}
state.activeIndex =
(state.activeIndex - 1 + state.matches.length) % state.matches.length;
updateActiveHighlight();
updateCounter();
scrollActiveIntoView();
return getState();
}
function observeDocumentChanges() {
if (
state.observer ||
!document.body ||
typeof MutationObserver !== "function"
) {
return;
}
state.observer = new MutationObserver((mutations) => {
if (!state.isOpen || !state.query) {
return;
}
if (mutations.every((mutation) => isFindPanelNode(mutation.target))) {
return;
}
debounceSearch(state.query);
});
state.observer.observe(document.body, {
childList: true,
characterData: true,
subtree: true,
});
}
function stopObservingDocumentChanges() {
state.observer?.disconnect();
state.observer = null;
}
function open() {
if (!state.enabled) {
return getState();
}
const panel = ensurePanel();
panel.setAttribute("data-visible", "true");
state.isOpen = true;
observeDocumentChanges();
requestAnimationFrame(() => {
state.input?.focus();
state.input?.select();
});
if (state.input?.value) {
runSearch(state.input.value);
} else {
updateCounter();
}
return getState();
}
function close() {
clearTimeout(state.searchTimer);
state.isOpen = false;
state.panel?.removeAttribute("data-visible");
clearHighlights();
stopObservingDocumentChanges();
state.matches = [];
state.activeIndex = -1;
state.truncated = false;
updateCounter();
return getState();
}
function search(query) {
if (state.input) {
state.input.value = query;
}
return runSearch(query);
}
function getFindShortcutAction(event) {
const userAgent = navigator.userAgent || "";
const isMac = /macintosh|mac os x/i.test(userAgent);
const hasModifier = isMac
? event.metaKey && !event.ctrlKey
: event.ctrlKey && !event.metaKey;
if (!hasModifier || event.altKey) {
return "";
}
const key = event.key?.toLowerCase();
if (key === "f" && !event.shiftKey) {
return "open";
}
if (key === "g") {
return event.shiftKey ? "previous" : "next";
}
return "";
}
function handleFindShortcut(event) {
const action = getFindShortcutAction(event);
if (!action) {
return;
}
event.preventDefault();
event.stopPropagation();
window.pakeFind[action]();
}
window.pakeFind = {
open,
close,
next,
previous,
search,
getState,
getFindShortcutAction,
};
if (state.enabled) {
document.addEventListener("keydown", handleFindShortcut, true);
}
})();