您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add phone icon to phone numbers for dialing (click) and copying (context menu / right click)
当前为
- // ==UserScript==
- // @name Click to Dial Phone Icon
- // @namespace https://schlomo.schapiro.org/
- // @version 2024.11.01.01
- // @description Add phone icon to phone numbers for dialing (click) and copying (context menu / right click)
- // @author Schlomo Schapiro
- // @match *://*/*
- // @grant none
- // @run-at document-start
- // @homepageURL https://schlomo.schapiro.org/
- // @icon https://gist.githubusercontent.com/schlomo/d68c66b6cf9e5d8a258e22c9bf31bf3f/raw/~click-to-dial.svg
- // ==/UserScript==
- /**
- This script adds a phone icon before every phone number found. Click to dial via your local soft phone (what
- happens is the same as clicking on a tel: link), right click to copy the phone number to the clipboard.
- My motivation for writing this was to have a solution that I can trust because the code is simple enough to read.
- Copyright 2024 Schlomo Schapiro
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- (() => {
- // add debug to URL query or hash to activate debug output in console
- const hasDebug = window.location.search.includes('debug') ||
- window.location.hash.includes('debug');
- const debug = hasDebug
- ? (...args) => console.log('[Click to Dial]', ...args)
- : () => { };
- const ID_SUFFIX = Math.random().toString(36).substring(2, 8);
- const PREFIX = 'click-to-dial';
- const PHONE_ICON_CLASS = `${PREFIX}-icon-${ID_SUFFIX}`;
- const PHONE_NUMBER_CLASS = `${PREFIX}-number-${ID_SUFFIX}`;
- const NO_PHONE_ICON_CLASS = `${PREFIX}-no-icon-${ID_SUFFIX}`;
- const PHONE_REGEX = new RegExp(String.raw`
- (?<!\d) # Negative lookbehind: not preceded by a digit
- (?! # Negative lookahead: don't match timestamps
- (?:
- \d{1,2} # Hours: 1-2 digits
- [:.] # Separator (colon or dot)
- (?:
- \d{2} # Minutes only (HH:MM)
- | # OR
- \d{2}[:.]\d{2} # Minutes and seconds (HH:MM:SS)
- )
- )
- )
- (?:\+|0) # Must start with + or 0
- (?:
- [().\s-\/]*\d # Digits with optional separators
- | # OR
- w # 'w' character for wait/pause
- ){9,25} # Length: 9-25 digits
- (?!\d) # Negative lookahead: not followed by a digit
- `.replace(/\s+#.+/g, '') // Remove comments
- .replace(/\s+/g, ''), // Remove whitespace
- 'g' // Global flag
- );
- let processCount = 0;
- function injectStyles() {
- const styleId = `${PREFIX}-style-${ID_SUFFIX}`;
- if (document.getElementById(styleId)) return;
- const style = document.createElement('style');
- style.id = styleId;
- style.textContent = `
- .${PHONE_ICON_CLASS} {
- display: inline-block;
- margin-right: 0.3em;
- text-decoration: none !important;
- cursor: pointer;
- }
- `;
- document.head.appendChild(style);
- }
- function showCopiedTooltip(event, phoneNumber) {
- const tooltip = document.createElement('div');
- tooltip.className = NO_PHONE_ICON_CLASS;
- tooltip.textContent = `Phone number ${phoneNumber} copied!`;
- tooltip.style.cssText = `
- position: fixed;
- left: ${event.clientX + 10}px;
- top: ${event.clientY + 10}px;
- background: #333;
- color: white;
- padding: 5px 10px;
- border-radius: 4px;
- font-size: 12px;
- z-index: 10000;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.2s;
- `;
- document.body.appendChild(tooltip);
- requestAnimationFrame(() => {
- tooltip.style.opacity = '1';
- setTimeout(() => {
- tooltip.style.opacity = '0';
- setTimeout(() => tooltip.remove(), 200);
- }, 1000);
- });
- }
- function createPhoneIcon(phoneNumber) {
- const icon = document.createElement('a');
- icon.className = PHONE_ICON_CLASS;
- const cleanNumber = phoneNumber.replace(/[^\d+]/g, '').replace('+490', '+49');
- icon.href = 'tel:' + cleanNumber;
- icon.textContent = '☎';
- icon.addEventListener('click', (event) => {
- event.preventDefault();
- event.stopPropagation();
- window.location.href = icon.href;
- return false;
- });
- icon.addEventListener('contextmenu', (event) => {
- event.preventDefault();
- event.stopPropagation();
- navigator.clipboard.writeText(cleanNumber).then(() => showCopiedTooltip(event, cleanNumber));
- return false;
- });
- return icon;
- }
- function isEditable(element) {
- if (!element) return false;
- if (element.getAttribute('contenteditable') === 'false') return false;
- return element.isContentEditable ||
- (element.tagName === 'INPUT' && !['button', 'submit', 'reset', 'hidden'].includes(element.type?.toLowerCase())) ||
- element.tagName === 'TEXTAREA' ||
- (element.getAttribute('role') === 'textbox' && element.getAttribute('contenteditable') !== 'false') ||
- element.getAttribute('contenteditable') === 'true' ||
- element.classList?.contains('ql-editor') ||
- element.classList?.contains('cke_editable') ||
- element.classList?.contains('tox-edit-area') ||
- element.classList?.contains('kix-page-content-wrapper') ||
- element.classList?.contains('waffle-content-pane');
- }
- function processTextNode(node) {
- const matches = Array.from(node.textContent.matchAll(PHONE_REGEX));
- if (!matches.length) return 0;
- debug('Found numbers: ',
- matches.map(m => m[0]).join(', ')
- );
- const fragment = document.createDocumentFragment();
- let lastIndex = 0;
- matches.forEach(match => {
- if (match.index > lastIndex) {
- fragment.appendChild(document.createTextNode(node.textContent.slice(lastIndex, match.index)));
- }
- const span = document.createElement('span');
- span.className = PHONE_NUMBER_CLASS;
- const icon = createPhoneIcon(match[0]);
- span.appendChild(icon);
- span.appendChild(document.createTextNode(match[0]));
- fragment.appendChild(span);
- lastIndex = match.index + match[0].length;
- });
- if (lastIndex < node.textContent.length) {
- fragment.appendChild(document.createTextNode(node.textContent.slice(lastIndex)));
- }
- node.parentNode.replaceChild(fragment, node);
- return matches.length;
- }
- function traverseDOM(node) {
- let nodesProcessed = 0;
- let phoneNumbersFound = 0;
- function traverse(node) {
- if (node.nodeType === Node.TEXT_NODE &&
- !node.parentElement.classList.contains(PHONE_NUMBER_CLASS) &&
- !node.parentElement.querySelector(`.${PHONE_ICON_CLASS}`) &&
- !node.parentElement.classList.contains(NO_PHONE_ICON_CLASS)) { // Exclude elements with the tooltip class
- nodesProcessed++;
- phoneNumbersFound += processTextNode(node);
- } else if (node.nodeType === Node.ELEMENT_NODE && !isEditable(node)) {
- for (let child = node.firstChild; child; child = child.nextSibling) {
- traverse(child);
- }
- }
- }
- traverse(node);
- return { nodesProcessed, phoneNumbersFound };
- }
- function processPhoneNumbers() {
- const startTime = performance.now();
- const { nodesProcessed, phoneNumbersFound } = traverseDOM(document.body);
- if (phoneNumbersFound > 0) {
- const duration = performance.now() - startTime;
- debug(
- `Phone number processing #${++processCount}:`,
- `${phoneNumbersFound} numbers in ${nodesProcessed} nodes, ${duration.toFixed(1)}ms`
- );
- }
- }
- function debounce(func, wait) {
- let timeout;
- return function (...args) {
- clearTimeout(timeout);
- timeout = setTimeout(() => func.apply(this, args), wait);
- };
- }
- document.addEventListener('DOMContentLoaded', () => {
- injectStyles();
- const debouncedProcessPhoneNumbers = debounce(processPhoneNumbers, 300);
- debouncedProcessPhoneNumbers();
- new MutationObserver(mutations => {
- if (mutations.some(m =>
- m.type === 'childList' ||
- (m.type === 'attributes' && ['contenteditable', 'role', 'class'].includes(m.attributeName))
- )) {
- debouncedProcessPhoneNumbers();
- }
- }).observe(document.body, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['contenteditable', 'role', 'class']
- });
- document.addEventListener('focus', debouncedProcessPhoneNumbers, true);
- // Reprocess periodically to catch any missed phone numbers
- setInterval(debouncedProcessPhoneNumbers, 5000);
- });
- })();