Skip to main content

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

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

Что делает

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

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

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

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

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

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

    Как выглядит

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

    Установка

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

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

        Скрипт активен на всём поддомене 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 — кнопка отображается всегда (более удобно).