CSN Odoo Enhanced Functionality

Combines phone number hyperlinks, email copying, and name copying functionalities for CSN Odoo

As of 2024-07-25. See the latest version.

// ==UserScript==
// @name        CSN Odoo Enhanced Functionality
// @namespace   http://tampermonkey.net/
// @version     3.4
// @description Combines phone number hyperlinks, email copying, and name copying functionalities for CSN Odoo
// @match       https://csnodoo.shopcsntv.com/*
// @grant       GM_addStyle
// @grant       GM_setClipboard
// @require     https://kit.fontawesome.com/YOUR_KIT_ID.js
// @run-at      document-idle
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    // Add Font Awesome styles and custom CSS
    GM_addStyle(`
        @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css');
        .copy-icon, .email-copy-icon {
            cursor: pointer;
            position: relative;
            display: inline-block;
        }
        h1 span.copy-icon i.fas.fa-copy {
            font-size: 18px;
            padding-bottom: 4px;
            padding-left: 5px;
        }
        h1 span.copy-icon {
            padding-left: 10px;
        }
        i.fas.fa-copy {
            font-size: 13px;
            vertical-align: middle;
        }
        i.fas.fa-copy {
            font-size: 12px;
        }
        div.o_row span.copy-icon {
            padding-right: 8px;
        }
        span.tooltiptext {
            font-size: 12px;
        }
        span.email-copy-icon {
            padding-right: 5px;
        }
        .copy-icon .tooltiptext, .email-copy-icon .tooltiptext {
            visibility: hidden;
            width: 140px;
            background-color: #555;
            color: #fff;
            text-align: center;
            border-radius: 6px;
            padding: 5px;
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            margin-left: -70px;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .copy-icon .tooltiptext::after, .email-copy-icon .tooltiptext::after {
            content: "";
            position: absolute;
            top: 100%;
            left: 50%;
            margin-left: -5px;
            border-width: 5px;
            border-style: solid;
            border-color: #555 transparent transparent transparent;
        }
        .copy-icon:hover .tooltiptext, .email-copy-icon:hover .tooltiptext {
            visibility: visible;
            opacity: 1;
        }
    `);

    function formatPhoneNumber(phoneNumberString) {
        return phoneNumberString.replace(/\D/g, '');
    }

    function isValidPhoneNumber(str) {
        const phoneRegex = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4}$/;
        return phoneRegex.test(str);
    }

    function isValidEmail(email) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
    }

    function createAnonymousPhoneLink(phoneNumber) {
        const formattedNumber = formatPhoneNumber(phoneNumber);
        const phoneLink = document.createElement('a');
        phoneLink.href = 'tel:*67' + formattedNumber;
        phoneLink.style.textDecoration = 'none';
        phoneLink.innerHTML = '<i class="fas fa-phone-slash"></i>';
        phoneLink.title = 'Call with *67';
        phoneLink.setAttribute('aria-label', 'Anonymous Call');
        phoneLink.style.marginLeft = '10px';
        return phoneLink;
    }

    function createCopyIcon(text, isEmail = false) {
        const copyIcon = document.createElement('span');
        copyIcon.className = isEmail ? 'email-copy-icon' : 'copy-icon';
        copyIcon.innerHTML = '<i class="fas fa-copy"></i>';
        copyIcon.setAttribute('aria-label', `Copy ${isEmail ? 'email' : 'text'}`);
        const tooltipSpan = document.createElement('span');
        tooltipSpan.className = 'tooltiptext';
        tooltipSpan.textContent = 'Copy to clipboard';
        copyIcon.appendChild(tooltipSpan);
        copyIcon.addEventListener('click', (e) => {
            e.stopPropagation();
            copyToClipboard(text, tooltipSpan);
        });
        return copyIcon;
    }

    function copyToClipboard(text, tooltipElement) {
        let formattedText = text;
        if (isValidPhoneNumber(text)) {
            formattedText = formatPhoneNumber(text);
        }
        navigator.clipboard.writeText(formattedText).then(() => {
            console.log('Text copied to clipboard:', formattedText);
            tooltipElement.textContent = 'Copied!';
            setTimeout(() => {
                tooltipElement.textContent = 'Copy to clipboard';
            }, 2000);
        }).catch(err => {
            console.error('Could not copy text: ', err);
            tooltipElement.textContent = 'Failed to copy';
            setTimeout(() => {
                tooltipElement.textContent = 'Copy to clipboard';
            }, 2000);
        });
    }

    function convertToClickableLink(element) {
        const spanContent = element.textContent.trim();
        if (isValidPhoneNumber(spanContent)) {
            const container = document.createElement('span');
            const copyIcon = createCopyIcon(spanContent);
            const phoneText = document.createTextNode(spanContent);
            const anonymousPhoneLink = createAnonymousPhoneLink(spanContent);
            container.appendChild(copyIcon);
            container.appendChild(phoneText);
            container.appendChild(anonymousPhoneLink);
            element.parentNode.replaceChild(container, element);
        }
    }

    function addEmailCopyIcon(emailLink) {
        const emailAddress = emailLink.textContent.trim();
        if (isValidEmail(emailAddress) &&
            (!emailLink.previousElementSibling || !emailLink.previousElementSibling.classList.contains('email-copy-icon'))) {
            const copyIcon = createCopyIcon(emailAddress, true);
            emailLink.parentNode.insertBefore(copyIcon, emailLink);
        }
    }

    function addCopyIconToNameSpans() {
        const nameSpans = document.querySelectorAll('span[class="o_field_char o_field_widget o_required_modifier"]');
        nameSpans.forEach(span => {
            const nameText = span.textContent.trim();
            if (nameText && !span.nextElementSibling?.classList.contains('copy-icon')) {
                const copyIcon = createCopyIcon(nameText);
                span.parentNode.insertBefore(copyIcon, span.nextSibling);
            }
        });
    }

    function updateButtonClass() {
        const buttons = document.querySelectorAll('button[name="cloudcti_open_outgoing_notification"]');
        buttons.forEach(button => {
            if (button.classList.contains('mb4')) {
                button.classList.remove('mb4');
            }
        });
    }

    function observeDOMChanges() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            const spans = node.querySelectorAll('span');
                            spans.forEach(convertToClickableLink);
                            const emailLinks = node.querySelectorAll('a.o_field_email');
                            emailLinks.forEach(addEmailCopyIcon);
                            const nameSpans = node.querySelectorAll('span[class="o_field_char o_field_widget o_required_modifier"]');
                            nameSpans.forEach(span => {
                                const nameText = span.textContent.trim();
                                if (nameText && !span.nextElementSibling?.classList.contains('copy-icon')) {
                                    const copyIcon = createCopyIcon(nameText);
                                    span.parentNode.insertBefore(copyIcon, span.nextSibling);
                                }
                            });
                            updateButtonClass();
                        }
                    });
                }
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Initial run
    addCopyIconToNameSpans();
    updateButtonClass();
    observeDOMChanges();
})();