返回首頁 

Universal Clickable Phone Numbers (International)

Script to replace phone numbers with clickable links, works with any international numbers on any pages.


Install this script?
// ==UserScript==// @name             Универсальные кликабельные номера телефонов на странице (Международные)// @name:en          Universal Clickable Phone Numbers (International)// @namespace        http://tampermonkey.net/// @version          2.6// @description      Скрипт для замены номеров телефона на кликабельные ссылки, работает с любыми международными номерами на любых страницах.// @description:en   Script to replace phone numbers with clickable links, works with any international numbers on any pages.// @author           Coffee_Feather// @match            *://*/*// @grant            none// @noframes// @license          MIT// ==/UserScript==(function() {'use strict';const config = {phonePatterns: [{type: 'RU_FULL',regex: /(?:\+7|8|7)[\s()-]*\d{3}[\s()-]*\d{3}[\s()-]*\d{2}[\s()-]*\d{2}/g,minDigits: 11,maxDigits: 11,normalize: (match) => {let digits = match.replace(/\D/g, '');digits = digits.startsWith('8') ? '+7' + digits.slice(1) : '+7' + digits.slice(1);return digits;},disabled: false},{type: 'UA_FULL',regex: /(?:\+380|380|\+38|38)(?:[\s()-]*\d){9,10}/g,minDigits: 12,maxDigits: 12,normalize: (match) => {let digits = match.replace(/\D/g, '');// Обработка формата +38XX...if (digits.startsWith('38') && digits.length === 10) {return '+380' + digits.slice(2);}// Обработка формата +380XX...if (digits.startsWith('380')) {return '+' + digits;}// Все остальные случаи считаем невалиднымиreturn null;},disabled: false},{type: 'INTERNATIONAL',regex: /\+\d{1,4}(?:[\s()-]*\d){6,14}/g, // Разрешены разделители между цифрамиminDigits: 7, // Минимальная длина: код страны (1-4) + номер (6+)maxDigits: 15, // Максимальная длина: код (4) + номер (11)normalize: function(match) {const digits = match.replace(/\D/g, '');const codeLength = digits.match(/^\+\d+/)?.[0].length || 0;// Проверка длины номера (без кода страны)const numberLength = digits.length - codeLength;if (numberLength < 6 || numberLength > 11) return null;return digits;},disabled: true}],excludedDomains: [''],allowedParents: ['DIV', 'SPAN', 'P', 'TD', 'LABEL'],forbiddenParents: ['A', 'SCRIPT', 'STYLE', 'TEXTAREA'],forbiddenClasses: ['sidebar', 'sidebar__menu-item', 'compose-button', 'settings'],debounceTime: 300};function isForbidden(node) {if (!node) return true;if (node.classList && config.forbiddenClasses.some(c => node.classList.contains(c))) {return true;}if (config.forbiddenParents.includes(node.tagName)) {return true;}let parent = node.parentNode;while (parent && parent !== document.body) {if (parent.classList && config.forbiddenClasses.some(c => parent.classList.contains(c))) {return true;}if (config.forbiddenParents.includes(parent.tagName)) {return true;}parent = parent.parentNode;}return false;}function safeReplace(textNode) {try {const text = textNode.nodeValue;const parent = textNode.parentNode;if (!parent || !config.allowedParents.includes(parent.tagName) || isForbidden(parent)) return;const matches = [];config.phonePatterns.forEach(pattern => {if (pattern.disabled) return; // Пропускаем отключенные модулиlet match;const regex = new RegExp(pattern.regex.source, 'g');while ((match = regex.exec(text)) !== null) {const [fullMatch] = match;const digitsOnly = fullMatch.replace(/\D/g, '');// Проверка длины для конкретного паттернаconst length = digitsOnly.length;if (length < pattern.minDigits || length > pattern.maxDigits) continue;// Нормализация номераconst normalized = pattern.normalize(fullMatch);matches.push({original: fullMatch,index: match.index,normalized,type: pattern.type});}});if (matches.length === 0) return;// Сортировка и удаление пересеченийmatches.sort((a, b) => a.index - b.index);const filteredMatches = [];let lastEnd = -1;for (const match of matches) {if (match.index > lastEnd) {filteredMatches.push(match);lastEnd = match.index + match.original.length;}}// Создание DOM элементовconst fragment = document.createDocumentFragment();let lastIndex = 0;filteredMatches.forEach(({original, index, normalized, type}) => {if (index > lastIndex) {fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)));}const link = document.createElement('a');link.href = `tel:${normalized}`;link.dataset.phoneType = type;link.style.cssText = 'color: inherit; text-decoration: inherit;';link.textContent = original;fragment.appendChild(link);lastIndex = index + original.length;});if (lastIndex < text.length) {fragment.appendChild(document.createTextNode(text.slice(lastIndex)));}parent.replaceChild(fragment, textNode);} catch(e) {console.error('Phone replace error:', e);}}function processor() {// Проверка исключений для домена и всех поддоменовconst isExcluded = config.excludedDomains.some(domain => {const host = location.hostname.toLowerCase();const checkDomain = domain.toLowerCase().replace(/^\./, '');// Проверка точного совпадения или поддоменаreturn host === checkDomain ||host.endsWith(`.${checkDomain}`);});if (isExcluded) {console.log('Домен исключен:', location.hostname);return;}const walker = document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,{acceptNode(node) {if (isForbidden(node.parentElement)) return NodeFilter.FILTER_REJECT;// Фильтруем отключенные паттерныconst activePatterns = config.phonePatterns.filter(p => !p.disabled);return activePatterns.some(pattern => {const regex = new RegExp(pattern.regex.source);return regex.test(node.nodeValue);}) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;}});const nodes = [];while (walker.nextNode()) nodes.push(walker.currentNode);nodes.reverse().forEach(safeReplace);}function debounce(func, wait) {let timeout;return (...args) => {clearTimeout(timeout);timeout = setTimeout(() => func.apply(this, args), wait);};}// Инициализацияif (!config.excludedDomains.includes(location.hostname)) {processor();const debouncedProcessor = debounce(processor, config.debounceTime);new MutationObserver(debouncedProcessor).observe(document.body, {childList: true,subtree: true,characterData: true});}})();