Skip to main content

GetCourse — кнопка копирования абсолютной ссылки (userscript)

GetCourse — кнопка копирования абсолютной ссылки (userscript)

Что делает

Добавляет в админке GetCourse рядом со ссылками в таблицах (например, в разделе CMS → Страницы) маленькую иконку «копировать». Клик по ней копирует абсолютный URL страницы (с доменом) в буфер обмена. — без необходимости открывать ссылку или вручную добавлять домен.

После копирования иконка на ~1 секунду превращается в зелёную галочку — визуальное подтверждение.

Зачем это нужно

В админке GC ссылки на страницы лендингов отображаются как относительные пути (/mai-extra-2200-08sDS5). Чтобы поделиться полным адресом, раньше приходилось:

  1. Открывать ссылку в новой вкладке.
  2. Копировать URL из адресной строки.
  3. Закрывать вкладку.

С этим скриптом — один клик прямо в списке.

Как выглядит

В колонке «Адрес» (или другой колонке со ссылками <td class="w5">) появляется ненавязчивая серая иконка справа от каждой ссылки. При наведении — становится голубой. После клика — кратко зелёная галочка.

Установка

  1. Поставьте расширение Tampermonkey:
  2. Откройте панель Tampermonkey → Создать новый скрипт.
  3. Удалите шаблон, вставьте код ниже целиком, сохраните Ctrl + S.
  4. Перезагрузите страницу админки GetCourse (Ctrl + F5 для жёсткого обновления).
  5. Кнопка появится автоматически рядом с каждой ссылкой в таблицах.

На каких страницах работает

Скрипт активен на всём поддомене fitnessmama.school/pl/* и на любом аккаунте *.getcourse.ru/pl/*. Конкретный селектор — td.w5 > a.not-pjax-link, поэтому кнопка появляется именно там, где этот класс используется (списки страниц CMS, и аналогичные таблицы).

Если нужно расширить на другие места админки — отредактируйте селектор в функции process() или замените td.w5 на нужный класс.

Технические детали

  • Иконка — inline SVG (не emoji), чтобы GC не превращал её в HTML-сущность &#128203;.
  • MutationObserver следит за добавлением новых строк (пагинация, AJAX-фильтры) — кнопки появляются и для них.
  • Атрибут data-gc-copy-added защищает от дублей при повторных запусках.
  • Fallback на document.execCommand('copy') если navigator.clipboard недоступен (старые браузеры или несекьюрный контекст).

Полный код скрипта

// ==UserScript==
// @name         GetCourse — копировать абсолютную ссылку
// @namespace    fitnessmama.school
// @version      1.3
// @description  Кнопка копирования абсолютного URL рядом со ссылками в админке GetCourse
// @match        https://fitnessmama.school/pl/*
// @match        https://*.getcourse.ru/pl/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const BTN_CLASS = 'gc-copy-abs-link-btn';
    const PROCESSED_ATTR = 'data-gc-copy-added';

    const style = document.createElement('style');
    style.textContent = `
        td.w5 > a.not-pjax-link[${PROCESSED_ATTR}] {
            margin-right: 2px;
        }
        .${BTN_CLASS} {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 18px;
            height: 18px;
            padding: 0;
            margin: 0 0 0 2px;
            border: none;
            border-radius: 4px;
            background: transparent;
            color: #8a96a8;
            cursor: pointer;
            vertical-align: -4px;
            opacity: 0.7;
            transition: background .15s ease, color .15s ease;
        }
        .${BTN_CLASS}:hover { opacity: 1; background: #e8f0fe; color: #1a73e8; }
        .${BTN_CLASS}:active { background: #d2e3fc; }
        .${BTN_CLASS}.copied { opacity: 1; background: #e6f4ea; color: #188038; }
        .${BTN_CLASS} svg { width: 13px; height: 13px; display: block; }
    `;
    document.head.appendChild(style);

    const SVG_NS = 'http://www.w3.org/2000/svg';

    function svg(paths) {
        const s = document.createElementNS(SVG_NS, 'svg');
        s.setAttribute('viewBox', '0 0 24 24');
        s.setAttribute('fill', 'none');
        s.setAttribute('stroke', 'currentColor');
        s.setAttribute('stroke-width', '2');
        s.setAttribute('stroke-linecap', 'round');
        s.setAttribute('stroke-linejoin', 'round');
        paths.forEach(([tag, attrs]) => {
            const el = document.createElementNS(SVG_NS, tag);
            for (const k in attrs) el.setAttribute(k, attrs[k]);
            s.appendChild(el);
        });
        return s;
    }

    const iconCopy = () => svg([
        ['rect', { x: 9, y: 9, width: 13, height: 13, rx: 2 }],
        ['path', { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }],
    ]);

    const iconOk = () => {
        const s = svg([['polyline', { points: '20 6 9 17 4 12' }]]);
        s.setAttribute('stroke-width', '3');
        return s;
    };

    function makeButton(absUrl) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = BTN_CLASS;
        btn.title = 'Скопировать ссылку';
        btn.setAttribute('aria-label', 'Скопировать ' + absUrl);
        btn.appendChild(iconCopy());
        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();
            try {
                await navigator.clipboard.writeText(absUrl);
            } catch {
                const ta = document.createElement('textarea');
                ta.value = absUrl;
                ta.style.position = 'fixed';
                ta.style.opacity = '0';
                document.body.appendChild(ta);
                ta.select();
                document.execCommand('copy');
                ta.remove();
            }
            btn.classList.add('copied');
            btn.replaceChildren(iconOk());
            setTimeout(() => {
                btn.classList.remove('copied');
                btn.replaceChildren(iconCopy());
            }, 1100);
        });
        return btn;
    }

    function process(root) {
        if (!root || root.nodeType !== 1) root = document.body;
        const links = root.querySelectorAll('td.w5 > a.not-pjax-link[href]');
        links.forEach((a) => {
            if (a.hasAttribute(PROCESSED_ATTR)) return;
            const href = a.getAttribute('href');
            if (!href || href.startsWith('javascript:') || href.startsWith('#')) return;
            let absUrl;
            try { absUrl = new URL(href, location.origin).href; } catch { return; }
            a.setAttribute(PROCESSED_ATTR, '1');
            const btn = makeButton(absUrl);
            if (a.nextSibling) a.parentNode.insertBefore(btn, a.nextSibling);
            else a.parentNode.appendChild(btn);
        });
    }

    function cleanupOld() {
        document.querySelectorAll('td.w5 > a.not-pjax-link').forEach((a) => {
            let n = a.nextSibling;
            while (n) {
                const next = n.nextSibling;
                if (n.nodeType === 3 && /^\s*&#128203;\s*$/.test(n.textContent)) {
                    n.remove();
                } else if (n.nodeType === 1 && n.tagName === 'BUTTON' && (n.textContent.includes('📋') || n.textContent.includes('&#128203;'))) {
                    n.remove();
                }
                n = next;
            }
        });
    }

    cleanupOld();
    process(document.body);

    const observer = new MutationObserver((mutations) => {
        for (const m of mutations) {
            m.addedNodes.forEach((node) => {
                if (node.nodeType === 1) process(node);
            });
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();

Версия

v1.3 — кнопка отображается всегда (полупрозрачная по умолчанию, ярче при наведении).

История

  • v1.0 — первая версия с emoji 📋 (заменена из-за того, что GC экранировал эмодзи в &#128203;).
  • v1.1 — переход на SVG-иконки.
  • v1.2 — кнопка появляется только при наведении на строку.
  • v1.3 — кнопка отображается всегда (более удобно).