import './utils/map-upsert-polyfill.js';
import './utils/setup-pdf-worker.js';
import { categories } from './config/tools.js';
import { dom, switchView, hideAlert } from './ui.js';
import { ShortcutsManager } from './logic/shortcuts.js';
import { createIcons, icons } from 'lucide';
import '@phosphor-icons/web/regular';
import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css';
import {
escapeHtml,
formatShortcutDisplay,
formatStars,
} from './utils/helpers.js';
import {
initI18n,
applyTranslations,
rewriteLinks,
injectLanguageSwitcher,
t,
trText,
} from './i18n/index.js';
import {
loadRuntimeConfig,
isToolDisabled,
isCurrentPageDisabled,
} from './utils/disabled-tools.js';
import {
getStoredItem,
setStoredItem,
removeStoredItem,
} from './utils/safe-storage.js';
import { sanitizeTranslationHtml } from './utils/translation-html.js';
import type { ToolCategory } from '@/types';
const FAVORITE_STAR_PATHS = [
[
'off',
'M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z',
],
[
'on',
'M234.29,114.85l-45,38.83L203,211.75a16.4,16.4,0,0,1-24.5,17.82L128,198.49,77.47,229.57A16.4,16.4,0,0,1,53,211.75l13.76-58.07-45-38.83A16.46,16.46,0,0,1,31.08,86l59-4.76,22.76-55.08a16.36,16.36,0,0,1,30.27,0l22.75,55.08,59,4.76a16.46,16.46,0,0,1,9.37,28.86Z',
],
] as const;
declare const __BRAND_NAME__: string;
const init = async () => {
await initI18n();
await loadRuntimeConfig();
injectLanguageSwitcher();
applyTranslations();
if (isCurrentPageDisabled()) {
document.title = t('disabledTool.title') || trText('Tool Unavailable');
const main = document.querySelector('main') || document.body;
const heading =
t('disabledTool.heading') || trText('This tool has been disabled');
const message =
t('disabledTool.message') ||
trText(
'This tool is not available in your deployment. Contact your administrator for more information.'
);
const backHome = t('disabledTool.backHome') || trText('Back to Home');
main.innerHTML = `
${sanitizeTranslationHtml(heading)}
${sanitizeTranslationHtml(message)}
${sanitizeTranslationHtml(backHome)}
`;
return;
}
if (__SIMPLE_MODE__) {
const hideBrandingSections = () => {
const heroSection = document.getElementById('hero-section');
if (heroSection) {
heroSection.style.display = 'none';
}
const githubLink = document.querySelector(
'a[href*="github.com/alam00000/bentopdf"]'
);
if (githubLink) {
(githubLink as HTMLElement).style.display = 'none';
}
const featuresSection = document.getElementById('features-section');
if (featuresSection) {
featuresSection.style.display = 'none';
}
const securitySection = document.getElementById(
'security-compliance-section'
);
if (securitySection) {
securitySection.style.display = 'none';
}
const faqSection = document.getElementById('faq-accordion');
if (faqSection) {
faqSection.style.display = 'none';
}
const testimonialsSection = document.getElementById(
'testimonials-section'
);
if (testimonialsSection) {
testimonialsSection.style.display = 'none';
}
const supportSection = document.getElementById('support-section');
if (supportSection) {
supportSection.style.display = 'none';
}
// Hide "Used by companies" section
const usedBySection = document.querySelector(
'.hide-section'
) as HTMLElement;
if (usedBySection) {
usedBySection.style.display = 'none';
}
const sectionDividers = document.querySelectorAll('.section-divider');
sectionDividers.forEach((divider) => {
(divider as HTMLElement).style.display = 'none';
});
const brandName = __BRAND_NAME__ || 'BentoPDF';
document.title = trText('{{brandName}} - {{title}}', {
brandName,
title: t('simpleMode.title'),
});
const toolsHeader = document.getElementById('tools-header');
if (toolsHeader) {
const title = toolsHeader.querySelector('h2');
const subtitle = toolsHeader.querySelector('p');
if (title) {
title.textContent = t('simpleMode.title');
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
}
if (subtitle) {
subtitle.textContent = t('simpleMode.subtitle');
subtitle.className = 'text-lg text-gray-400';
}
}
const app = document.getElementById('app');
if (app) {
app.style.paddingTop = '1rem';
}
};
hideBrandingSections();
}
// Hide shortcuts buttons on mobile devices (Android/iOS)
// exclude iPad -> users can connect keyboard and use shortcuts
const isMobile = /Android|iPhone|iPod/i.test(navigator.userAgent);
const keyboardShortcutBtn = document.getElementById('shortcut');
const shortcutSettingsBtn = document.getElementById('open-shortcuts-btn');
if (isMobile) {
if (keyboardShortcutBtn) keyboardShortcutBtn.style.display = 'none';
if (shortcutSettingsBtn) shortcutSettingsBtn.style.display = 'none';
} else {
if (keyboardShortcutBtn) {
keyboardShortcutBtn.textContent = navigator.userAgent
.toUpperCase()
.includes('MAC')
? '⌘ + K'
: 'Ctrl + K';
}
}
const categoryTranslationKeys: Record = {
Favorites: 'tools:categories.favorites',
'Popular Tools': 'tools:categories.popularTools',
'Edit & Annotate': 'tools:categories.editAnnotate',
'Convert to PDF': 'tools:categories.convertToPdf',
'Convert from PDF': 'tools:categories.convertFromPdf',
'Organize & Manage': 'tools:categories.organizeManage',
'Optimize & Repair': 'tools:categories.optimizeRepair',
'Secure PDF': 'tools:categories.securePdf',
};
const toolTranslationKeys: Record = {
'PDF Workflow Builder': 'tools:pdfWorkflow',
'PDF Multi Tool': 'tools:pdfMultiTool',
'Merge PDF': 'tools:mergePdf',
'Split PDF': 'tools:splitPdf',
'Compress PDF': 'tools:compressPdf',
'PDF Editor': 'tools:pdfEditor',
'Edit PDF Text': 'tools:editPdfText',
'JPG to PDF': 'tools:jpgToPdf',
'Sign PDF': 'tools:signPdf',
'Crop PDF': 'tools:cropPdf',
'Extract Pages': 'tools:extractPages',
'Duplicate & Organize': 'tools:duplicateOrganize',
'Delete Pages': 'tools:deletePages',
'Edit Bookmarks': 'tools:editBookmarks',
'Table of Contents': 'tools:tableOfContents',
'Page Numbers': 'tools:pageNumbers',
'Add Page Labels': 'tools:addPageLabels',
'Add Watermark': 'tools:addWatermark',
'Header & Footer': 'tools:headerFooter',
'Invert Colors': 'tools:invertColors',
'Background Color': 'tools:backgroundColor',
'Change Text Color': 'tools:changeTextColor',
'Add Stamps': 'tools:addStamps',
'Bates Numbering': 'tools:batesNumbering',
'Remove Annotations': 'tools:removeAnnotations',
'PDF Form Filler': 'tools:pdfFormFiller',
'Create PDF Form': 'tools:createPdfForm',
'Remove Blank Pages': 'tools:removeBlankPages',
'Images to PDF': 'tools:imageToPdf',
'PNG to PDF': 'tools:pngToPdf',
'WebP to PDF': 'tools:webpToPdf',
'SVG to PDF': 'tools:svgToPdf',
'BMP to PDF': 'tools:bmpToPdf',
'HEIC to PDF': 'tools:heicToPdf',
'TIFF to PDF': 'tools:tiffToPdf',
'Text to PDF': 'tools:textToPdf',
'JSON to PDF': 'tools:jsonToPdf',
'PDF to JPG': 'tools:pdfToJpg',
'PDF to PNG': 'tools:pdfToPng',
'PDF to WebP': 'tools:pdfToWebp',
'PDF to BMP': 'tools:pdfToBmp',
'PDF to TIFF': 'tools:pdfToTiff',
'PDF to CBZ': 'tools:pdfToCbz',
'PDF to Greyscale': 'tools:pdfToGreyscale',
'PDF to JSON': 'tools:pdfToJson',
'OCR PDF': 'tools:ocrPdf',
'Alternate & Mix Pages': 'tools:alternateMerge',
'Duplex Collate': 'tools:duplexCollate',
'PDF Overlay': 'tools:pdfOverlay',
'Organize & Duplicate': 'tools:duplicateOrganize',
'Add Attachments': 'tools:addAttachments',
'Extract Attachments': 'tools:extractAttachments',
'Edit Attachments': 'tools:editAttachments',
'Divide Pages': 'tools:dividePages',
'Add Blank Page': 'tools:addBlankPage',
'Reverse Pages': 'tools:reversePages',
'Rotate PDF': 'tools:rotatePdf',
'Rotate by Custom Degrees': 'tools:rotateCustom',
'N-Up PDF': 'tools:nUpPdf',
'Combine to Single Page': 'tools:combineToSinglePage',
'View Metadata': 'tools:viewMetadata',
'Edit Metadata': 'tools:editMetadata',
'PDFs to ZIP': 'tools:pdfsToZip',
'Compare PDFs': 'tools:comparePdfs',
'Posterize PDF': 'tools:posterizePdf',
'Fix Page Size': 'tools:fixPageSize',
'Linearize PDF': 'tools:linearizePdf',
'Page Dimensions': 'tools:pageDimensions',
'Remove Restrictions': 'tools:removeRestrictions',
'Repair PDF': 'tools:repairPdf',
'Encrypt PDF': 'tools:encryptPdf',
'Sanitize PDF': 'tools:sanitizePdf',
'Protect PDF': 'tools:protectPdf',
'Unlock PDF': 'tools:unlockPdf',
'Decrypt PDF': 'tools:decryptPdf',
'Flatten PDF': 'tools:flattenPdf',
'Remove Metadata': 'tools:removeMetadata',
'Change Permissions': 'tools:changePermissions',
'Email to PDF': 'tools:emailToPdf',
'Font to Outline': 'tools:fontToOutline',
'Deskew PDF': 'tools:deskewPdf',
'Digital Signature': 'tools:digitalSignPdf',
'Validate Signature': 'tools:validateSignaturePdf',
'Timestamp PDF': 'tools:timestampPdf',
'Scanner Effect': 'tools:scannerEffect',
'Adjust Colors': 'tools:adjustColors',
'Markdown to PDF': 'tools:markdownToPdf',
'PDF Booklet': 'tools:pdfBooklet',
'Word to PDF': 'tools:wordToPdf',
'Excel to PDF': 'tools:excelToPdf',
'PowerPoint to PDF': 'tools:powerpointToPdf',
'XPS to PDF': 'tools:xpsToPdf',
'MOBI to PDF': 'tools:mobiToPdf',
'EPUB to PDF': 'tools:epubToPdf',
'FB2 to PDF': 'tools:fb2ToPdf',
'CBZ to PDF': 'tools:cbzToPdf',
'WPD to PDF': 'tools:wpdToPdf',
'WPS to PDF': 'tools:wpsToPdf',
'XML to PDF': 'tools:xmlToPdf',
'Pages to PDF': 'tools:pagesToPdf',
'ODG to PDF': 'tools:odgToPdf',
'ODS to PDF': 'tools:odsToPdf',
'ODP to PDF': 'tools:odpToPdf',
'PUB to PDF': 'tools:pubToPdf',
'VSD to PDF': 'tools:vsdToPdf',
'PSD to PDF': 'tools:psdToPdf',
'ODT to PDF': 'tools:odtToPdf',
'CSV to PDF': 'tools:csvToPdf',
'RTF to PDF': 'tools:rtfToPdf',
'PDF to SVG': 'tools:pdfToSvg',
'PDF to CSV': 'tools:pdfToCsv',
'PDF to Excel': 'tools:pdfToExcel',
'PDF to Text': 'tools:pdfToText',
'Extract Tables': 'tools:extractTables',
'PDF to Word': 'tools:pdfToWord',
'Extract Images': 'tools:extractImages',
'PDF to Markdown': 'tools:pdfToMarkdown',
'Prepare PDF for AI': 'tools:preparePdfForAi',
'PDF OCG': 'tools:pdfOcg',
'PDF to PDF/A': 'tools:pdfToPdfa',
'Rasterize PDF': 'tools:rasterizePdf',
};
// Homepage-only tool grid rendering (not used on individual tool pages)
if (dom.toolGrid) {
dom.toolGrid.textContent = '';
let collapsedCategories: string[] = [];
try {
const stored = getStoredItem('collapsedCategories');
if (stored) collapsedCategories = JSON.parse(stored);
} catch {
removeStoredItem('collapsedCategories');
}
function saveCollapsedCategories() {
setStoredItem('collapsedCategories', JSON.stringify(collapsedCategories));
}
const filteredCategories = categories
.map((category) => ({
...category,
tools: category.tools.filter((tool) => !isToolDisabled(tool.id)),
}))
.filter((category) => category.tools.length > 0);
const availableTools = new Map();
filteredCategories.forEach((category) =>
category.tools.forEach((tool) => {
if (!availableTools.has(tool.id)) availableTools.set(tool.id, tool);
})
);
let favoriteToolIds: string[] = [];
let unavailableFavoriteIds: string[] = [];
function loadFavorites() {
let stored: unknown = [];
try {
stored = JSON.parse(getStoredItem('favoriteTools') || '[]');
} catch {
removeStoredItem('favoriteTools');
}
const ids = Array.isArray(stored)
? [
...new Set(
stored.filter((id): id is string => typeof id === 'string')
),
]
: [];
favoriteToolIds = ids.filter((id) => availableTools.has(id));
unavailableFavoriteIds = ids.filter((id) => !availableTools.has(id));
}
loadFavorites();
function createFavoriteButton(toolId: string): HTMLButtonElement {
const button = document.createElement('button');
button.type = 'button';
button.className = 'tool-fav-btn';
button.dataset.favId = toolId;
button.appendChild(createStarIcon());
updateFavoriteButton(button);
return button;
}
function createStarIcon(): SVGSVGElement {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 256 256');
svg.setAttribute('aria-hidden', 'true');
for (const [kind, d] of FAVORITE_STAR_PATHS) {
const path = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute('class', `tool-fav-${kind}`);
path.setAttribute('d', d);
svg.appendChild(path);
}
return svg;
}
const favoritesHint = document.createElement('div');
favoritesHint.className = 'tool-fav-hint col-span-full';
const favoritesHintText = document.createElement('span');
const favoritesHintLabel = document.createElement('strong');
favoritesHintLabel.className = 'tool-fav-hint-label';
favoritesHintLabel.textContent = t('tools:favorites.hintLabel');
favoritesHintText.append(
favoritesHintLabel,
document.createTextNode(` ${t('tools:favorites.hint')}`)
);
const favoritesHintClose = document.createElement('button');
favoritesHintClose.type = 'button';
favoritesHintClose.className = 'tool-fav-hint-close';
favoritesHintClose.setAttribute(
'aria-label',
t('tools:favorites.hideHint')
);
const favoritesHintIcon = document.createElement('i');
favoritesHintIcon.className = 'ph ph-x';
favoritesHintIcon.setAttribute('aria-hidden', 'true');
favoritesHintClose.appendChild(favoritesHintIcon);
favoritesHint.append(favoritesHintText, favoritesHintClose);
favoritesHintClose.addEventListener('click', () => {
setStoredItem('favoritesHintHidden', 'true');
favoritesHint.remove();
});
function updateFavoritesHint() {
if (
favoriteToolIds.length > 0 ||
getStoredItem('favoritesHintHidden') === 'true'
) {
favoritesHint.remove();
return;
}
if (favoritesHint.isConnected) return;
const results = document.getElementById('search-results');
dom.toolGrid.insertBefore(
favoritesHint,
results ? results.nextSibling : dom.toolGrid.firstChild
);
}
const favoriteMenu = document.createElement('div');
favoriteMenu.className = 'tool-fav-menu';
const favoriteMenuButton = document.createElement('button');
favoriteMenuButton.type = 'button';
favoriteMenuButton.className = 'tool-fav-menu-btn';
const favoriteMenuLabel = document.createElement('span');
favoriteMenuButton.append(createStarIcon(), favoriteMenuLabel);
favoriteMenu.appendChild(favoriteMenuButton);
let favoriteMenuStar: HTMLElement | null = null;
function openFavoriteMenu(tile: HTMLElement) {
const star = tile.querySelector('.tool-fav-btn');
if (!star) return;
favoriteMenuStar = star;
const active = favoriteToolIds.includes(star.dataset.favId || '');
favoriteMenuButton.classList.toggle('is-favorite', active);
favoriteMenuLabel.textContent = active
? t('tools:favorites.remove')
: t('tools:favorites.add');
tile.appendChild(favoriteMenu);
if (navigator.userActivation?.hasBeenActive) navigator.vibrate?.(10);
}
function closeFavoriteMenu() {
favoriteMenu.remove();
favoriteMenuStar = null;
}
favoriteMenuButton.addEventListener('click', (event) => {
event.stopPropagation();
const star = favoriteMenuStar;
closeFavoriteMenu();
if (star) toggleFavorite(star);
});
function updateFavoriteButton(button: HTMLElement) {
const active = favoriteToolIds.includes(button.dataset.favId || '');
button.classList.toggle('is-favorite', active);
button.setAttribute('aria-pressed', String(active));
const label = active
? t('tools:favorites.remove')
: t('tools:favorites.add');
button.setAttribute('aria-label', label);
button.title = label;
}
function renderFavorites() {
const tools = favoriteToolIds.flatMap((id) => {
const tool = availableTools.get(id);
return tool ? [tool] : [];
});
if (tools.length === 0) return;
const group = createCategoryGroup({ name: 'Favorites', tools });
const searchBox = document.getElementById(
'search-bar'
) as HTMLInputElement | null;
if (searchBox?.value.trim()) group.style.display = 'none';
const results = document.getElementById('search-results');
dom.toolGrid.insertBefore(
group,
results ? results.nextSibling : dom.toolGrid.firstChild
);
createIcons({ icons });
}
function toggleFavorite(button: HTMLElement) {
const toolId = button.dataset.favId;
const tool = toolId ? availableTools.get(toolId) : undefined;
if (!toolId || !tool) return;
const adding = !favoriteToolIds.includes(toolId);
favoriteToolIds = adding
? [...favoriteToolIds, toolId]
: favoriteToolIds.filter((id) => id !== toolId);
setStoredItem(
'favoriteTools',
JSON.stringify([...favoriteToolIds, ...unavailableFavoriteIds])
);
const anchorTop = button.getBoundingClientRect().top;
const keyboardFocus = button.matches(':focus-visible');
const group = dom.toolGrid.querySelector(
':scope > .is-cat-favorites'
);
let nextFocus: HTMLElement | null = null;
if (adding && group) {
group
.querySelector('.category-tools')
?.appendChild(createToolTile(tool));
createIcons({ icons });
} else if (adding) {
renderFavorites();
} else if (group) {
const tile = group
.querySelector(`.tool-fav-btn[data-fav-id="${CSS.escape(toolId)}"]`)
?.closest('.tool-tile');
if (tile?.contains(button)) {
const neighbor =
tile.nextElementSibling ?? tile.previousElementSibling;
nextFocus =
neighbor?.querySelector('.tool-fav-btn') ??
group.nextElementSibling?.querySelector(
'.category-header'
) ??
null;
}
tile?.remove();
if (favoriteToolIds.length === 0) group.remove();
}
dom.toolGrid
.querySelectorAll('.tool-fav-btn')
.forEach((other) => {
if (other.dataset.favId === toolId) updateFavoriteButton(other);
});
updateFavoritesHint();
if (button.isConnected) {
const shift = button.getBoundingClientRect().top - anchorTop;
if (shift) window.scrollBy({ top: shift, behavior: 'instant' });
} else if (keyboardFocus) {
nextFocus?.focus({ preventScroll: true });
}
}
function createToolTile(tool: ToolCategory['tools'][number]) {
const tile = document.createElement('div');
tile.className = 'tool-tile';
let toolCard: HTMLDivElement | HTMLAnchorElement;
if (tool.href) {
toolCard = document.createElement('a');
toolCard.href = tool.href;
toolCard.className =
'tool-card block bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center no-underline hover:shadow-lg transition duration-200';
} else {
toolCard = document.createElement('div');
toolCard.className =
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center hover:shadow-lg transition duration-200';
toolCard.dataset.toolId = getToolId(tool);
}
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
if (tool.icon.startsWith('ph-')) {
icon.className = `ph ${tool.icon} text-4xl mb-3 text-indigo-400`;
} else {
icon.setAttribute('data-lucide', tool.icon);
}
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
const toolKey = toolTranslationKeys[tool.name];
toolName.textContent = toolKey ? t(`${toolKey}.name`) : tool.name;
toolCard.append(icon, toolName);
if (tool.subtitle) {
const toolSubtitle = document.createElement('p');
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
toolSubtitle.textContent = toolKey
? t(`${toolKey}.subtitle`)
: tool.subtitle;
toolCard.appendChild(toolSubtitle);
}
tile.append(toolCard, createFavoriteButton(tool.id));
return tile;
}
function createCategoryGroup(category: ToolCategory): HTMLDivElement {
const categoryGroup = document.createElement('div');
const categorySlug = category.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
categoryGroup.className = `category-group col-span-full is-cat-${categorySlug}`;
const header = document.createElement('button');
header.className = 'category-header';
header.type = 'button';
const title = document.createElement('span');
const categoryKey = categoryTranslationKeys[category.name];
title.textContent = categoryKey ? t(categoryKey) : category.name;
const chevron = document.createElement('i');
chevron.setAttribute('data-lucide', 'chevron-down');
chevron.className =
'category-chevron w-5 h-5 text-gray-400 transition-transform duration-300';
header.append(title, chevron);
const toolsContainer = document.createElement('div');
toolsContainer.className =
'category-tools grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
const isCollapsed = collapsedCategories.includes(category.name);
if (isCollapsed) {
categoryGroup.classList.add('collapsed');
toolsContainer.style.maxHeight = '0px';
}
toolsContainer.addEventListener('transitionend', (e) => {
if ((e as TransitionEvent).propertyName !== 'max-height') return;
if (!categoryGroup.classList.contains('collapsed')) {
toolsContainer.style.maxHeight = 'none';
toolsContainer.style.overflow = 'visible';
}
});
header.addEventListener('click', () => {
const collapsed = categoryGroup.classList.toggle('collapsed');
if (collapsed) {
toolsContainer.style.maxHeight = toolsContainer.scrollHeight + 'px';
toolsContainer.style.overflow = 'hidden';
requestAnimationFrame(() => {
toolsContainer.style.maxHeight = '0px';
});
if (!collapsedCategories.includes(category.name)) {
collapsedCategories.push(category.name);
}
} else {
toolsContainer.style.overflow = 'hidden';
toolsContainer.style.maxHeight = toolsContainer.scrollHeight + 'px';
collapsedCategories = collapsedCategories.filter(
(n) => n !== category.name
);
}
saveCollapsedCategories();
});
category.tools.forEach((tool) => {
toolsContainer.appendChild(createToolTile(tool));
});
categoryGroup.append(header, toolsContainer);
if (!isCollapsed) {
toolsContainer.style.maxHeight = 'none';
toolsContainer.style.overflow = 'visible';
}
return categoryGroup;
}
filteredCategories.forEach((category) => {
dom.toolGrid.appendChild(createCategoryGroup(category));
});
const searchBar = document.getElementById('search-bar');
const searchResultsContainer = document.createElement('div');
searchResultsContainer.id = 'search-results';
searchResultsContainer.className =
'hidden grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6 col-span-full';
dom.toolGrid.insertBefore(searchResultsContainer, dom.toolGrid.firstChild);
renderFavorites();
updateFavoritesHint();
searchBar.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = searchBar.value.toLowerCase().trim();
closeFavoriteMenu();
const categoryGroups = dom.toolGrid.querySelectorAll(
':scope > .category-group'
);
if (!searchTerm) {
searchResultsContainer.classList.add('hidden');
searchResultsContainer.innerHTML = '';
categoryGroups.forEach((group) => {
(group as HTMLElement).style.display = '';
const toolCards = group.querySelectorAll('.tool-card');
toolCards.forEach((card) => {
(card as HTMLElement).style.display = '';
});
});
return;
}
categoryGroups.forEach((group) => {
(group as HTMLElement).style.display = 'none';
});
searchResultsContainer.innerHTML = '';
searchResultsContainer.classList.remove('hidden');
const seenToolIds = new Set();
const allTools: HTMLElement[] = [];
categoryGroups.forEach((group) => {
const toolCards = Array.from(group.querySelectorAll('.tool-card'));
toolCards.forEach((card) => {
const toolName = (
card.querySelector('h3')?.textContent || ''
).toLowerCase();
const toolSubtitle = (
card.querySelector('p')?.textContent || ''
).toLowerCase();
const toolHref =
(card as HTMLAnchorElement).href ||
(card as HTMLElement).dataset.toolId ||
'';
const toolId =
toolHref.split('/').pop()?.replace('.html', '') || toolName;
const isMatch =
toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
const isDuplicate = seenToolIds.has(toolId);
if (isMatch && !isDuplicate) {
seenToolIds.add(toolId);
allTools.push(
(card.closest('.tool-tile') ?? card).cloneNode(
true
) as HTMLElement
);
}
});
});
allTools.forEach((tool) => {
searchResultsContainer.appendChild(tool);
});
createIcons({ icons });
});
window.addEventListener('keydown', function (e) {
const key = e.key.toLowerCase();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const isCtrlK = e.ctrlKey && key === 'k';
const isCmdK = isMac && e.metaKey && key === 'k';
if (isCtrlK || isCmdK) {
e.preventDefault();
searchBar.focus();
}
});
dom.toolGrid.addEventListener('click', (event) => {
// All tools now use href and navigate directly - no modal handling needed
const favoriteButton = (event.target as Element).closest(
'.tool-fav-btn'
);
if (favoriteButton) toggleFavorite(favoriteButton);
});
let pressTimer = 0;
let pressStart: { x: number; y: number } | null = null;
let suppressClick = false;
const touchOnly = window.matchMedia('(hover: none)');
function cancelPress() {
window.clearTimeout(pressTimer);
pressStart = null;
}
function pressedTile(target: EventTarget | null): HTMLElement | null {
const element = target instanceof Element ? target : null;
if (!element || element.closest('.tool-fav-btn, .tool-fav-menu')) {
return null;
}
return element.closest('.tool-tile');
}
dom.toolGrid.addEventListener('pointerdown', (event) => {
suppressClick = false;
if (event.pointerType !== 'touch' || !touchOnly.matches) return;
const tile = pressedTile(event.target);
if (!tile) return;
pressStart = { x: event.clientX, y: event.clientY };
pressTimer = window.setTimeout(() => {
pressStart = null;
suppressClick = true;
openFavoriteMenu(tile);
}, 500);
});
dom.toolGrid.addEventListener('pointermove', (event) => {
if (
pressStart &&
Math.hypot(event.clientX - pressStart.x, event.clientY - pressStart.y) >
10
) {
cancelPress();
}
});
dom.toolGrid.addEventListener('pointerup', cancelPress);
dom.toolGrid.addEventListener('pointercancel', cancelPress);
dom.toolGrid.addEventListener('contextmenu', (event) => {
if (!touchOnly.matches) return;
const tile = pressedTile(event.target);
if (!tile) return;
event.preventDefault();
cancelPress();
if (!tile.contains(favoriteMenu)) {
suppressClick = true;
openFavoriteMenu(tile);
}
});
dom.toolGrid.addEventListener(
'click',
(event) => {
if (!suppressClick) return;
suppressClick = false;
event.preventDefault();
event.stopPropagation();
},
true
);
document.addEventListener('pointerdown', (event) => {
if (
favoriteMenu.isConnected &&
!(event.target instanceof Node && favoriteMenu.contains(event.target))
) {
closeFavoriteMenu();
}
});
window.addEventListener('scroll', closeFavoriteMenu, { passive: true });
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeFavoriteMenu();
});
window.addEventListener('storage', (event) => {
if (
event.key !== null &&
event.key !== 'favoriteTools' &&
event.key !== 'favoritesHintHidden'
) {
return;
}
closeFavoriteMenu();
loadFavorites();
dom.toolGrid.querySelector(':scope > .is-cat-favorites')?.remove();
renderFavorites();
dom.toolGrid
.querySelectorAll('.tool-fav-btn')
.forEach(updateFavoriteButton);
updateFavoritesHint();
});
}
if (dom.backToGridBtn) {
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
}
if (dom.alertOkBtn) {
dom.alertOkBtn.addEventListener('click', hideAlert);
}
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
faqAccordion.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const questionButton = e.target.closest('.faq-question');
if (!questionButton) return;
const faqItem = questionButton.parentElement;
const answer = faqItem.querySelector('.faq-answer');
faqItem.classList.toggle('open');
if (faqItem.classList.contains('open')) {
answer.style.maxHeight = answer.scrollHeight + 'px';
} else {
answer.style.maxHeight = '0px';
}
});
}
const faqDetails =
document.querySelectorAll('details.faq-d');
if (
faqDetails.length > 0 &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches
) {
faqDetails.forEach((detail) => {
const summary = detail.querySelector('summary');
const body = detail.querySelector('.faq-d-a');
if (!summary || !body) return;
let animation: Animation | null = null;
summary.addEventListener('click', (event) => {
event.preventDefault();
animation?.cancel();
const wasOpen = detail.open;
if (!wasOpen) detail.open = true;
const fullHeight = `${body.scrollHeight}px`;
const fullPadding = window.getComputedStyle(body).paddingBottom;
const collapsed = { height: '0px', paddingBottom: '0px', opacity: 0 };
const expanded = {
height: fullHeight,
paddingBottom: fullPadding,
opacity: 1,
};
body.style.overflow = 'hidden';
animation = body.animate(
wasOpen ? [expanded, collapsed] : [collapsed, expanded],
{ duration: 280, easing: 'cubic-bezier(0.2, 0.7, 0.2, 1)' }
);
animation.onfinish = () => {
if (wasOpen) detail.open = false;
body.style.overflow = '';
animation = null;
};
});
});
}
createIcons({ icons });
console.log('Please share our tool and share the love!');
const githubStarsElements = [
document.getElementById('github-stars-desktop'),
document.getElementById('github-stars-mobile'),
];
if (
githubStarsElements.some((el) => el) &&
!__SIMPLE_MODE__ &&
!__DISABLE_GITHUB_STARS__
) {
fetch('https://api.github.com/repos/alam00000/bentopdf')
.then((response) => response.json())
.then((data) => {
if (data.stargazers_count !== undefined) {
const formattedStars = formatStars(data.stargazers_count);
githubStarsElements.forEach((el) => {
if (el) el.textContent = formattedStars;
});
}
})
.catch(() => {
githubStarsElements.forEach((el) => {
if (el) el.textContent = '-';
});
});
}
// Initialize Shortcuts System
ShortcutsManager.init();
// Tab switching for settings modal
const shortcutsTabBtn = document.getElementById('shortcuts-tab-btn');
const preferencesTabBtn = document.getElementById('preferences-tab-btn');
const shortcutsTabContent = document.getElementById('shortcuts-tab-content');
const preferencesTabContent = document.getElementById(
'preferences-tab-content'
);
const shortcutsTabFooter = document.getElementById('shortcuts-tab-footer');
const preferencesTabFooter = document.getElementById(
'preferences-tab-footer'
);
const resetShortcutsBtn = document.getElementById('reset-shortcuts-btn');
if (shortcutsTabBtn && preferencesTabBtn) {
shortcutsTabBtn.addEventListener('click', () => {
shortcutsTabBtn.classList.add('bg-indigo-600', 'text-white');
shortcutsTabBtn.classList.remove('text-gray-300');
preferencesTabBtn.classList.remove('bg-indigo-600', 'text-white');
preferencesTabBtn.classList.add('text-gray-300');
shortcutsTabContent?.classList.remove('hidden');
preferencesTabContent?.classList.add('hidden');
shortcutsTabFooter?.classList.remove('hidden');
preferencesTabFooter?.classList.add('hidden');
resetShortcutsBtn?.classList.remove('hidden');
});
preferencesTabBtn.addEventListener('click', () => {
preferencesTabBtn.classList.add('bg-indigo-600', 'text-white');
preferencesTabBtn.classList.remove('text-gray-300');
shortcutsTabBtn.classList.remove('bg-indigo-600', 'text-white');
shortcutsTabBtn.classList.add('text-gray-300');
preferencesTabContent?.classList.remove('hidden');
shortcutsTabContent?.classList.add('hidden');
preferencesTabFooter?.classList.remove('hidden');
shortcutsTabFooter?.classList.add('hidden');
resetShortcutsBtn?.classList.add('hidden');
});
}
// Full-width toggle functionality
const fullWidthToggle = document.getElementById(
'full-width-toggle'
) as HTMLInputElement;
const toolInterface = document.getElementById('tool-interface');
const savedFullWidth = getStoredItem('fullWidthMode') !== 'false';
if (fullWidthToggle) {
fullWidthToggle.checked = savedFullWidth;
applyFullWidthMode(savedFullWidth);
}
function applyFullWidthMode(enabled: boolean) {
if (toolInterface) {
if (enabled) {
toolInterface.classList.remove('max-w-4xl');
} else {
toolInterface.classList.add('max-w-4xl');
}
}
// Apply to all page uploaders
const pageUploaders = document.querySelectorAll(
'#tool-uploader, #signature-editor'
);
pageUploaders.forEach((uploader) => {
if (enabled) {
uploader.classList.remove('max-w-2xl', 'max-w-5xl');
} else {
// Restore original max-width (most are max-w-2xl, add-stamps is max-w-5xl)
if (
!uploader.classList.contains('max-w-2xl') &&
!uploader.classList.contains('max-w-5xl')
) {
uploader.classList.add('max-w-2xl');
}
}
});
}
if (fullWidthToggle) {
fullWidthToggle.addEventListener('change', (e) => {
const enabled = (e.target as HTMLInputElement).checked;
setStoredItem('fullWidthMode', enabled.toString());
applyFullWidthMode(enabled);
});
}
const compactModeToggle = document.getElementById(
'compact-mode-toggle'
) as HTMLInputElement;
const savedCompactMode = getStoredItem('compactMode') === 'true';
if (compactModeToggle) {
compactModeToggle.checked = savedCompactMode;
}
applyCompactMode(savedCompactMode);
function applyCompactMode(enabled: boolean) {
if (dom.toolGrid) {
dom.toolGrid.classList.toggle('compact-mode', enabled);
dom.toolGrid
.querySelectorAll('.category-group:not(.collapsed) .category-tools')
.forEach((container) => {
(container as HTMLElement).style.maxHeight = 'none';
});
}
}
if (compactModeToggle) {
compactModeToggle.addEventListener('change', (e) => {
const enabled = (e.target as HTMLInputElement).checked;
setStoredItem('compactMode', enabled.toString());
applyCompactMode(enabled);
});
}
// Shortcuts UI Handlers
if (dom.openShortcutsBtn) {
dom.openShortcutsBtn.addEventListener('click', () => {
renderShortcutsList();
dom.shortcutsModal.classList.remove('hidden');
});
}
if (dom.closeShortcutsModalBtn) {
dom.closeShortcutsModalBtn.addEventListener('click', () => {
dom.shortcutsModal.classList.add('hidden');
});
}
// Close modal on outside click
if (dom.shortcutsModal) {
dom.shortcutsModal.addEventListener('click', (e) => {
if (e.target === dom.shortcutsModal) {
dom.shortcutsModal.classList.add('hidden');
}
});
}
if (dom.resetShortcutsBtn) {
dom.resetShortcutsBtn.addEventListener('click', async () => {
const confirmed = await showWarningModal(
t('settings.warnings.resetTitle'),
t('settings.warnings.resetMessage'),
true
);
if (confirmed) {
ShortcutsManager.reset();
renderShortcutsList();
}
});
}
if (dom.exportShortcutsBtn) {
dom.exportShortcutsBtn.addEventListener('click', () => {
ShortcutsManager.exportSettings();
});
}
if (dom.importShortcutsBtn) {
dom.importShortcutsBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target?.result as string;
if (ShortcutsManager.importSettings(content)) {
renderShortcutsList();
await showWarningModal(
t('settings.warnings.importSuccessTitle'),
t('settings.warnings.importSuccessMessage'),
false
);
} else {
await showWarningModal(
t('settings.warnings.importFailTitle'),
t('settings.warnings.importFailMessage'),
false
);
}
};
reader.readAsText(file);
}
};
input.click();
});
}
if (dom.shortcutSearch) {
dom.shortcutSearch.addEventListener('input', (e) => {
const term = (e.target as HTMLInputElement).value.toLowerCase();
const sections = dom.shortcutsList.querySelectorAll('.category-section');
sections.forEach((section) => {
const items = section.querySelectorAll('.shortcut-item');
let visibleCount = 0;
items.forEach((item) => {
const text = item.textContent?.toLowerCase() || '';
if (text.includes(term)) {
item.classList.remove('hidden');
visibleCount++;
} else {
item.classList.add('hidden');
}
});
if (visibleCount === 0) {
section.classList.add('hidden');
} else {
section.classList.remove('hidden');
}
});
});
}
// Reserved shortcuts that commonly conflict with browser/OS functions
const RESERVED_SHORTCUTS: Record =
{
'mod+w': { mac: trText('Closes tab'), windows: trText('Closes tab') },
'mod+t': {
mac: trText('Opens new tab'),
windows: trText('Opens new tab'),
},
'mod+n': {
mac: trText('Opens new window'),
windows: trText('Opens new window'),
},
'mod+shift+n': {
mac: trText('Opens incognito window'),
windows: trText('Opens incognito window'),
},
'mod+q': { mac: trText('Quits application (cannot be overridden)') },
'mod+m': { mac: trText('Minimizes window') },
'mod+h': { mac: trText('Hides window') },
'mod+r': { mac: trText('Reloads page'), windows: trText('Reloads page') },
'mod+shift+r': {
mac: trText('Hard reloads page'),
windows: trText('Hard reloads page'),
},
'mod+l': {
mac: trText('Focuses address bar'),
windows: trText('Focuses address bar'),
},
'mod+d': {
mac: trText('Bookmarks page'),
windows: trText('Bookmarks page'),
},
'mod+shift+t': {
mac: trText('Reopens closed tab'),
windows: trText('Reopens closed tab'),
},
'mod+shift+w': {
mac: trText('Closes window'),
windows: trText('Closes window'),
},
'mod+tab': {
mac: trText('Switches tabs'),
windows: trText('Switches apps'),
},
'alt+f4': { windows: trText('Closes window') },
'ctrl+tab': {
mac: trText('Switches tabs'),
windows: trText('Switches tabs'),
},
};
function getReservedShortcutWarning(
combo: string,
isMac: boolean
): string | null {
const reserved = RESERVED_SHORTCUTS[combo];
if (!reserved) return null;
const description = isMac ? reserved.mac : reserved.windows;
if (!description) return null;
return description;
}
function showWarningModal(
title: string,
message: string,
confirmMode: boolean = true
): Promise {
return new Promise((resolve) => {
if (
!dom.warningModal ||
!dom.warningTitle ||
!dom.warningMessage ||
!dom.warningCancelBtn ||
!dom.warningConfirmBtn
) {
resolve(confirmMode ? confirm(message) : (alert(message), true));
return;
}
dom.warningTitle.textContent = title;
dom.warningMessage.innerHTML = sanitizeTranslationHtml(message);
dom.warningModal.classList.remove('hidden');
dom.warningModal.classList.add('flex');
if (confirmMode) {
dom.warningCancelBtn.style.display = '';
dom.warningConfirmBtn.textContent = t('warning.proceed');
} else {
dom.warningCancelBtn.style.display = 'none';
dom.warningConfirmBtn.textContent = t('alert.ok');
}
const handleConfirm = () => {
cleanup();
resolve(true);
};
const handleCancel = () => {
cleanup();
resolve(false);
};
const cleanup = () => {
dom.warningModal?.classList.add('hidden');
dom.warningModal?.classList.remove('flex');
dom.warningConfirmBtn?.removeEventListener('click', handleConfirm);
dom.warningCancelBtn?.removeEventListener('click', handleCancel);
};
dom.warningConfirmBtn.addEventListener('click', handleConfirm);
dom.warningCancelBtn.addEventListener('click', handleCancel);
// Close on backdrop click
dom.warningModal.addEventListener(
'click',
(e) => {
if (e.target === dom.warningModal) {
if (confirmMode) {
handleCancel();
} else {
handleConfirm();
}
}
},
{ once: true }
);
});
}
function getToolId(tool: { id?: string; href?: string }): string {
if (tool.id) return tool.id;
if (tool.href) {
const match = tool.href.match(/\/([^/]+)\.html$/);
return match ? match[1] : tool.href;
}
return 'unknown';
}
function renderShortcutsList() {
if (!dom.shortcutsList) return;
dom.shortcutsList.innerHTML = '';
const allShortcuts = ShortcutsManager.getAllShortcuts();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const shortcutCategories = categories
.map((category) => ({
...category,
tools: category.tools.filter((tool) => !isToolDisabled(tool.id)),
}))
.filter((category) => category.tools.length > 0);
const allTools = shortcutCategories.flatMap((c) => c.tools);
shortcutCategories.forEach((category) => {
const section = document.createElement('div');
section.className = 'category-section mb-6 last:mb-0';
const header = document.createElement('h3');
header.className =
'text-gray-400 text-xs font-bold uppercase tracking-wider mb-3 pl-1';
const categoryKey = categoryTranslationKeys[category.name];
header.textContent = categoryKey ? t(categoryKey) : category.name;
section.appendChild(header);
const itemsContainer = document.createElement('div');
itemsContainer.className = 'space-y-2';
section.appendChild(itemsContainer);
let hasTools = false;
category.tools.forEach((tool) => {
hasTools = true;
const toolId = getToolId(tool);
const currentShortcut = allShortcuts.get(toolId) || '';
const item = document.createElement('div');
item.className =
'shortcut-item flex items-center justify-between p-3 bg-gray-900 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors';
const left = document.createElement('div');
left.className = 'flex items-center gap-3';
const icon = document.createElement('i');
if (tool.icon.startsWith('ph-')) {
icon.className = `ph ${tool.icon} w-5 h-5 text-indigo-400`;
} else {
icon.className = 'w-5 h-5 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
}
const name = document.createElement('span');
name.className = 'text-gray-200 font-medium';
const toolKey = toolTranslationKeys[tool.name];
name.textContent = toolKey ? t(`${toolKey}.name`) : tool.name;
left.append(icon, name);
const right = document.createElement('div');
right.className = 'relative';
const input = document.createElement('input');
input.type = 'text';
input.className =
'shortcut-input w-32 bg-gray-800 border border-gray-600 text-white text-center text-sm rounded px-2 py-1 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all';
input.placeholder = t('settings.clickToSet');
input.value = formatShortcutDisplay(currentShortcut, isMac);
input.readOnly = true;
const clearBtn = document.createElement('button');
clearBtn.className =
'absolute -right-2 -top-2 bg-gray-700 hover:bg-red-600 text-white rounded-full p-0.5 hidden group-hover:block shadow-sm';
clearBtn.innerHTML = '';
if (currentShortcut) {
right.classList.add('group');
}
clearBtn.onclick = (e) => {
e.stopPropagation();
ShortcutsManager.setShortcut(toolId, '');
renderShortcutsList();
};
input.onkeydown = async (e) => {
e.preventDefault();
e.stopPropagation();
if (e.key === 'Backspace' || e.key === 'Delete') {
ShortcutsManager.setShortcut(toolId, '');
renderShortcutsList();
return;
}
const keys: string[] = [];
// On Mac: metaKey = Command, ctrlKey = Control
// On Windows/Linux: metaKey is rare, ctrlKey = Ctrl
if (isMac) {
if (e.metaKey) keys.push('mod'); // Command on Mac
if (e.ctrlKey) keys.push('ctrl'); // Control on Mac (separate from Command)
} else {
if (e.ctrlKey || e.metaKey) keys.push('mod'); // Ctrl on Windows/Linux
}
if (e.altKey) keys.push('alt');
if (e.shiftKey) keys.push('shift');
let key = e.key.toLowerCase();
if (e.altKey && e.code) {
if (e.code.startsWith('Key')) {
key = e.code.slice(3).toLowerCase();
} else if (e.code.startsWith('Digit')) {
key = e.code.slice(5);
}
}
const isModifier = ['control', 'shift', 'alt', 'meta'].includes(key);
const isDeadKey = key === 'dead' || key.startsWith('dead');
// Ignore dead keys (used for accented characters on Mac with Option key)
if (isDeadKey) {
input.value = formatShortcutDisplay(
ShortcutsManager.getShortcut(toolId) || '',
isMac
);
return;
}
if (!isModifier) {
keys.push(key);
}
const combo = keys.join('+');
input.value = formatShortcutDisplay(combo, isMac);
if (!isModifier) {
const existingToolId = ShortcutsManager.findToolByShortcut(combo);
if (existingToolId && existingToolId !== toolId) {
const existingTool = allTools.find(
(t) => getToolId(t) === existingToolId
);
const existingToolName = existingTool?.name || existingToolId;
const displayCombo = formatShortcutDisplay(combo, isMac);
const existingToolKey = existingTool
? toolTranslationKeys[existingTool.name]
: null;
const translatedToolName = existingToolKey
? t(`${existingToolKey}.name`)
: existingToolName;
await showWarningModal(
t('settings.warnings.alreadyInUse'),
`${escapeHtml(displayCombo)} ${t('settings.warnings.assignedTo')}
` +
`"${escapeHtml(translatedToolName)}"
` +
t('settings.warnings.chooseDifferent'),
false
);
input.value = formatShortcutDisplay(
ShortcutsManager.getShortcut(toolId) || '',
isMac
);
input.classList.remove('border-indigo-500', 'text-indigo-400');
input.blur();
return;
}
const reservedWarning = getReservedShortcutWarning(combo, isMac);
if (reservedWarning) {
const displayCombo = formatShortcutDisplay(combo, isMac);
const shouldProceed = await showWarningModal(
t('settings.warnings.reserved'),
`${escapeHtml(displayCombo)} ${t('settings.warnings.commonlyUsed')}
` +
`"${escapeHtml(reservedWarning)}"
` +
`${t('settings.warnings.unreliable')}
` +
t('settings.warnings.useAnyway')
);
if (!shouldProceed) {
// Revert display
input.value = formatShortcutDisplay(
ShortcutsManager.getShortcut(toolId) || '',
isMac
);
input.classList.remove('border-indigo-500', 'text-indigo-400');
input.blur();
return;
}
}
ShortcutsManager.setShortcut(toolId, combo);
// Re-render to update all inputs (show conflicts in real-time)
renderShortcutsList();
}
};
input.onkeyup = (e) => {
// If the user releases a modifier without pressing a main key, revert to saved
const key = e.key.toLowerCase();
if (['control', 'shift', 'alt', 'meta'].includes(key)) {
const currentSaved = ShortcutsManager.getShortcut(toolId);
}
};
input.onfocus = () => {
input.value = t('settings.pressKeys');
input.classList.add('border-indigo-500', 'text-indigo-400');
};
input.onblur = () => {
input.value = formatShortcutDisplay(
ShortcutsManager.getShortcut(toolId) || '',
isMac
);
input.classList.remove('border-indigo-500', 'text-indigo-400');
};
right.append(input);
if (currentShortcut) right.append(clearBtn);
item.append(left, right);
itemsContainer.appendChild(item);
});
if (hasTools) {
dom.shortcutsList.appendChild(section);
}
});
createIcons({ icons });
}
const scrollToTopBtn = document.getElementById('scroll-to-top-btn');
if (scrollToTopBtn) {
let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const currentScrollY = window.scrollY;
if (currentScrollY < lastScrollY && currentScrollY > 300) {
scrollToTopBtn.classList.add('visible');
} else {
scrollToTopBtn.classList.remove('visible');
}
lastScrollY = currentScrollY;
});
scrollToTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'instant',
});
});
}
// Rewrite links after all dynamic content is fully loaded
rewriteLinks();
};
window.addEventListener('load', init);