289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
(function () {
|
|
|
|
const $ = (q, c = document) => c.querySelector(q);
|
|
const $$ = (q, c = document) => Array.from(c.querySelectorAll(q));
|
|
|
|
// --- theme ---
|
|
const setTheme = (t) => {
|
|
document.documentElement.setAttribute('data-theme', t);
|
|
try { localStorage.setItem('theme', t) } catch { }
|
|
};
|
|
window.setTheme = setTheme;
|
|
|
|
const toast = (msg) => {
|
|
const el = $('#toast'); if (!el) return;
|
|
el.textContent = msg; el.classList.add('show');
|
|
clearTimeout(el._t); el._t = setTimeout(() => el.classList.remove('show'), 2000);
|
|
};
|
|
const host = () => `${location.protocol}//${location.host}`;
|
|
|
|
// --- IP validators ---
|
|
function isValidIPv4(ip) {
|
|
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(ip)) return false;
|
|
return ip.split('.').every(oct => {
|
|
if (oct.length > 1 && oct[0] === '0') return oct === '0';
|
|
const n = Number(oct);
|
|
return n >= 0 && n <= 255;
|
|
});
|
|
}
|
|
|
|
function isValidIPv6(ip) {
|
|
let work = ip;
|
|
const idx = work.lastIndexOf(':');
|
|
if (idx !== -1) {
|
|
const tail = work.slice(idx + 1);
|
|
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(tail)) {
|
|
if (!isValidIPv4(tail)) return false;
|
|
work = work.slice(0, idx) + ':0:0';
|
|
}
|
|
}
|
|
if (work.split('::').length > 2) return false;
|
|
const hasCompress = work.includes('::');
|
|
const parts = work.split(':').filter(Boolean);
|
|
if ((!hasCompress && parts.length !== 8) || (hasCompress && parts.length > 7)) return false;
|
|
return parts.every(g => /^[0-9a-fA-F]{1,4}$/.test(g));
|
|
}
|
|
|
|
function isValidIP(ip) {
|
|
const v = (ip || '').trim();
|
|
return isValidIPv4(v) || isValidIPv6(v);
|
|
}
|
|
|
|
// --- URL helpers ---
|
|
function normalizeUrlMaybe(v) {
|
|
const raw = (v || '').trim();
|
|
if (!raw) return '';
|
|
try {
|
|
const test = raw.includes('://') ? raw : `https://${raw}`;
|
|
const u = new URL(test);
|
|
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('scheme');
|
|
return u.toString();
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function buildLink(url, ip) {
|
|
if (!url || !ip) return '';
|
|
try {
|
|
const enc = encodeURIComponent(url);
|
|
const ipClean = (ip || '').trim();
|
|
return `${host()}/convert?url=${enc}&ip=${encodeURIComponent(ipClean)}`;
|
|
} catch { return ''; }
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const t = e.target.closest('[data-action="toggle-theme"]');
|
|
if (t) {
|
|
e.preventDefault();
|
|
const cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
setTheme(cur === 'dark' ? 'light' : 'dark');
|
|
}
|
|
});
|
|
|
|
const urlInput = $('#url-input');
|
|
const ipInput = $('#ip-input');
|
|
const ipPreset = $('#ip-preset');
|
|
const out = $('#generated-link');
|
|
const openBtn = $('#open-link');
|
|
const copyBtn = $('#copy-btn');
|
|
|
|
function showError(input, msg) {
|
|
const id = input.getAttribute('id');
|
|
const box = document.querySelector(`.error[data-error-for="${id}"]`);
|
|
if (box) box.textContent = msg || '';
|
|
input.setAttribute('aria-invalid', msg ? 'true' : 'false');
|
|
}
|
|
|
|
|
|
function updatePreview() {
|
|
const rawUrl = (urlInput?.value || '').trim();
|
|
const ip = (ipInput?.value || '').trim();
|
|
|
|
if (!ip || !isValidIP(ip)) {
|
|
if (out) out.value = '';
|
|
if (openBtn) {
|
|
openBtn.setAttribute('href', '#');
|
|
openBtn.setAttribute('aria-disabled', 'true');
|
|
openBtn.setAttribute('disabled', 'true');
|
|
}
|
|
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
|
|
$('.result-box')?.setAttribute('data-state', 'empty');
|
|
return;
|
|
}
|
|
|
|
const normalized = normalizeUrlMaybe(rawUrl);
|
|
const guessed = rawUrl ? (rawUrl.includes('://') ? rawUrl : `https://${rawUrl}`) : '';
|
|
const previewUrl = normalized || guessed;
|
|
|
|
if (!previewUrl) {
|
|
if (out) out.value = '';
|
|
if (openBtn) {
|
|
openBtn.setAttribute('href', '#');
|
|
openBtn.setAttribute('aria-disabled', 'true');
|
|
openBtn.setAttribute('disabled', 'true');
|
|
}
|
|
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
|
|
$('.result-box')?.setAttribute('data-state', 'empty');
|
|
return;
|
|
}
|
|
|
|
const link = buildLink(previewUrl, ip);
|
|
if (out) out.value = link;
|
|
|
|
const ok = !!normalized;
|
|
if (openBtn) {
|
|
if (ok) {
|
|
openBtn.setAttribute('href', link);
|
|
openBtn.setAttribute('aria-disabled', 'false');
|
|
openBtn.removeAttribute('disabled');
|
|
} else {
|
|
openBtn.setAttribute('href', '#');
|
|
openBtn.setAttribute('aria-disabled', 'true');
|
|
openBtn.setAttribute('disabled', 'true');
|
|
}
|
|
}
|
|
if (copyBtn) copyBtn.toggleAttribute('disabled', !ok);
|
|
$('.result-box')?.setAttribute('data-state', ok ? 'ready' : 'empty');
|
|
}
|
|
|
|
// live update
|
|
['input', 'change', 'blur'].forEach(evt => {
|
|
urlInput?.addEventListener(evt, updatePreview);
|
|
ipInput?.addEventListener(evt, updatePreview);
|
|
});
|
|
|
|
// presets
|
|
ipPreset?.addEventListener('change', () => {
|
|
const v = ipPreset.value;
|
|
if (!v) return;
|
|
if (v !== 'custom') ipInput.value = v;
|
|
ipInput.focus();
|
|
const ok = isValidIP(ipInput.value.trim());
|
|
showError(ipInput, ok ? '' : 'Invalid IP address');
|
|
updatePreview();
|
|
});
|
|
|
|
// event delegation
|
|
document.addEventListener('click', (e) => {
|
|
let t = e.target;
|
|
|
|
if (t.closest('[data-action="copy"]')) {
|
|
e.preventDefault();
|
|
const btn = t.closest('[data-action="copy"]');
|
|
const sel = btn.getAttribute('data-target') || '#generated-link';
|
|
const el = $(sel);
|
|
if (!el) return;
|
|
const text = el.value || el.textContent || '';
|
|
navigator.clipboard?.writeText(text).then(() => {
|
|
btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1200);
|
|
toast('Link copied');
|
|
}).catch(() => {
|
|
const range = document.createRange(); range.selectNodeContents(el);
|
|
const selObj = getSelection(); selObj.removeAllRanges(); selObj.addRange(range);
|
|
try { document.execCommand('copy'); toast('Link copied'); } catch { }
|
|
selObj.removeAllRanges();
|
|
});
|
|
}
|
|
|
|
if (t.closest('[data-action="copy-text"]')) {
|
|
e.preventDefault();
|
|
const btn = t.closest('[data-action="copy-text"]');
|
|
let text = btn.getAttribute('data-text') || '';
|
|
if (!text) return;
|
|
if (text.startsWith('/')) text = host() + text;
|
|
navigator.clipboard?.writeText(text).then(() => toast('Copied'));
|
|
}
|
|
|
|
if (t.closest('[data-action="clear"]')) {
|
|
e.preventDefault();
|
|
urlInput.value = '';
|
|
if (ipInput) ipInput.value = '';
|
|
if (ipPreset) ipPreset.value = '';
|
|
showError(urlInput, '');
|
|
showError(ipInput, '');
|
|
updatePreview();
|
|
urlInput.focus();
|
|
}
|
|
|
|
if (t.closest('[data-action="collapse"]')) {
|
|
e.preventDefault();
|
|
const btn = t.closest('[data-action="collapse"]');
|
|
const panel = $('#' + (btn.getAttribute('aria-controls') || ''));
|
|
if (!panel) return;
|
|
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
|
const next = !expanded;
|
|
btn.setAttribute('aria-expanded', next ? 'true' : 'false');
|
|
panel.hidden = !next;
|
|
panel.style.display = next ? '' : 'none';
|
|
const newLabel = next ? 'Collapse' : 'Expand';
|
|
btn.textContent = newLabel;
|
|
btn.setAttribute('aria-label', newLabel);
|
|
}
|
|
});
|
|
|
|
// field-level validation
|
|
urlInput?.addEventListener('blur', () => {
|
|
const raw = urlInput.value.trim();
|
|
if (!raw) return showError(urlInput, '');
|
|
const normalized = normalizeUrlMaybe(raw);
|
|
showError(urlInput, normalized ? '' : 'Invalid URL');
|
|
});
|
|
|
|
ipInput?.addEventListener('blur', () => {
|
|
const v = ipInput.value.trim();
|
|
if (!v) return showError(ipInput, '');
|
|
const ok = isValidIP(v);
|
|
showError(ipInput, ok ? '' : 'Invalid IP address');
|
|
if (!ok) $('.result-box')?.setAttribute('data-state', 'empty');
|
|
});
|
|
|
|
// init (preview + recent-list sync)
|
|
(function init() {
|
|
const serverLink = out?.value?.trim();
|
|
if (serverLink) {
|
|
$('.result-box')?.setAttribute('data-state', 'ready');
|
|
openBtn?.setAttribute('aria-disabled', 'false');
|
|
} else {
|
|
updatePreview();
|
|
}
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const btn = document.querySelector('[data-action="collapse"]');
|
|
const panelId = btn?.getAttribute('aria-controls') || '';
|
|
const panel = panelId ? document.getElementById(panelId) : null;
|
|
if (!btn || !panel) return;
|
|
|
|
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
|
panel.hidden = !expanded;
|
|
panel.style.display = expanded ? '' : 'none';
|
|
|
|
const label = expanded ? 'Collapse' : 'Expand';
|
|
btn.textContent = label;
|
|
btn.setAttribute('aria-label', label);
|
|
});
|
|
|
|
form?.addEventListener('submit', (e) => e.preventDefault());
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
|
|
const text = out?.value?.trim(); if (!text) return;
|
|
navigator.clipboard?.writeText(text).then(() => toast('Link copied'));
|
|
}
|
|
});
|
|
|
|
})();
|
|
|
|
// --- theme color sync (poza IIFE) ---
|
|
function updateThemeColor() {
|
|
const meta = document.querySelector('meta[name="theme-color"]');
|
|
if (!meta) return;
|
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
meta.setAttribute('content', isLight ? '#f6f8fb' : '#0f1115');
|
|
}
|
|
|
|
const _setTheme = setTheme;
|
|
setTheme = function (t) { _setTheme(t); updateThemeColor(); };
|
|
document.addEventListener('DOMContentLoaded', updateThemeColor);
|