Science Score: 44.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
    Found CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
    Found .zenodo.json file
  • DOI references
  • Academic publication links
  • Academic email domains
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (3.5%) to scientific vocabulary
Last synced: 6 months ago · JSON representation ·

Repository

Basic Info
  • Host: GitHub
  • Owner: magasine
  • Language: JavaScript
  • Default Branch: main
  • Size: 3.69 MB
Statistics
  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • Open Issues: 0
  • Releases: 0
Created 10 months ago · Last pushed 10 months ago
Metadata Files
Readme Citation

README.md

Citation Tool – Geração e Compartilhamento de Citações e Referências Web

O que é?

O Citation Tool é um bookmarklet (script executado diretamente do navegador) que oferece uma interface leve e interativa para capturar, formatar e compartilhar trechos de texto de páginas web. Ele é ideal para quem precisa salvar ou compartilhar conteúdos com referências estruturadas e visualmente organizadas — de forma rápida e sem sair da página atual.

Principais Funcionalidades

1) Captura inteligente de conteúdo

Detecta automaticamente o texto selecionado na página ou, alternativamente, recupera o conteúdo principal do site (ex: artigos, postagens e reportagens). Também permite extrair diretamente do clipboard (área de transferência) do navegador.

2) Formatos de citação personalizáveis

Converte o conteúdo capturado em diversos estilos, adaptados a diferentes contextos: - WhatsApp Format (formatação amigável para envio em mensagens), - Citação Acadêmica (inclui data de acesso e referência formal), - HTML e Markdown (ideal para blogs, wikis e sistemas de documentação), - Texto simples (plain text), - Twitter/X (respeita limite de caracteres e formato citacional).

3) Visualização e edição interativa

  • Exibe uma prévia do conteúdo formatado antes da cópia ou do compartilhamento, com opção de ajustes em tempo real.

4) Integração com serviços de leitura facilitada

  • Gera links diretos para serviços como PrintFriendly e Archive.is, permitindo compartilhar versões mais limpas e acessíveis da página original.

5) Coleção múltipla via clipboard

  • Modo especial para armazenar múltiplos trechos coletados ao longo da navegação, separando automaticamente com marcadores e organizando tudo em uma única saída citacional.

6) Botões de ação rápida

Com um clique, o usuário pode: - Copiar o conteúdo formatado para a área de transferência, - Compartilhar diretamente via WhatsApp, Twitter/X, e-mail ou gerar um QR Code com a citação.

7) Interface leve, responsiva e com suporte a modo escuro

  • A janela de operação é embutida na própria página, com design adaptável ao tema do sistema e totalmente removível após o uso.

Owner

  • Name: Manoel Garcia da Silveira Neto
  • Login: magasine
  • Kind: user
  • Location: Campinas/SP, Brasil

Trocar o crer pelo saber. Evoluir. Conquistar níveis mais elevados de consciência. Colaborar com a humanidade.

Citation (citationTool.js)

javascript: (() => {
  // Configurações e constantes
  const CONFIG = {
    BADGE_ID: "citation-tool",
    HOST_ID: "citation-tool-host",
    APP_INFO: {
      name: "Citation Tool",
      version: "v20250526", // sanitize function simplified
      credits: "by @magasine",
    },
    FORMATS: [
      { value: "default", text: "Whatsapp Format" },
      { value: "academic", text: "Academic Citation" },
      { value: "html", text: "HTML" },
      { value: "markdown", text: "Markdown" },
      { value: "plain", text: "Simple Text" },
      { value: "twitter", text: "Twitter/X" },
    ],
    READABILITY_SERVICES: [
      {
        name: "PrintFriendly",
        url: (url) =>
          `https://www.printfriendly.com/print/?url=${encodeURIComponent(url)}`,
      },
      {
        name: "Archive.is",
        url: (url) => `https://archive.is/${encodeURIComponent(url)}`,
      },
    ],
    CONTENT_SELECTORS: [
      "article",
      '[role="main"]',
      ".main-content",
      "#content",
      ".content",
      "main",
    ],
    CLIPBOARD_SEPARATOR: "\n---\n",
  };

  const state = {
    clipboardItems: [],
    captureMode: "selection",
    isMinimized: false,
    isDragging: false, // Estado para controlar o arrasto
    readabilityEnabled: true, // Novo estado para controlar a visibilidade do seletor
  };

  // Função de sanitização contextual para Trusted Types
  const sanitize = (str, context = "html") => {
    if (!str) return "";
    str = String(str);

    if (context === "html") {
      return str
        .replace(/&/g, "&")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        // .replace(/"/g, "&quot;") // simplificando
        // .replace(/'/g, "&#39;"); // simplificando
    }

    // Para outros contextos, apenas garantir que é uma string segura
    return str;
  };

  // Utils module
  const Utils = {
    copyToClipboard: async (text) => {
      try {
        if (navigator.clipboard?.writeText) {
          await navigator.clipboard.writeText(text);
          return true;
        }
        const textarea = document.createElement("textarea");
        textarea.value = text;
        textarea.style.position = "fixed";
        document.body.appendChild(textarea);
        textarea.select();
        const success = document.execCommand("copy");
        document.body.removeChild(textarea);
        return success;
      } catch (error) {
        console.error(`[${CONFIG.APP_INFO.name}] Copy error:`, error);
        return false;
      }
    },

    showFeedback: (shadowRoot, message, duration = 3000) => {
      let feedback = shadowRoot.getElementById("citation-feedback");
      if (!feedback) {
        feedback = document.createElement("div");
        feedback.id = "citation-feedback";
        feedback.className = "citation-feedback";
        shadowRoot.appendChild(feedback);
      }
      feedback.textContent = sanitize(message);
      feedback.style.opacity = "1";
      setTimeout(() => (feedback.style.opacity = "0"), duration);
    },

    getPageContent: () => {
      const selection = window.getSelection().toString().trim();
      if (selection) return sanitize(selection);
      for (const selector of CONFIG.CONTENT_SELECTORS) {
        const element = document.querySelector(selector);
        if (element) {
          const text = element.textContent.trim();
          const sanitizedText = sanitize(text);
          return sanitizedText.length > 280
            ? sanitizedText.substring(0, 280) + "..."
            : sanitizedText;
        }
      }
      return "No text selected or main content found.";
    },

    getClipboardText: async () => {
      try {
        const text = await navigator.clipboard?.readText();
        return text ? sanitize(text) : "Clipboard access not supported";
      } catch (error) {
        return "Could not access clipboard";
      }
    },

    // Detecta se o dispositivo é móvel
    isMobileDevice: () => {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
        navigator.userAgent
      );
    },

    // Função para log de debug
    debug: (message) => {
      console.log(`[${CONFIG.APP_INFO.name} DEBUG] ${message}`);
    },
  };

  // Citation module
  const Citation = {
    format: (format, text, title, url, service, includeLink) => {
      const separator = "(...)";
      const serviceUrl = service.url(url);
      const safeText = sanitize(text);
      const safeTitle = sanitize(title);
      const safeUrl = sanitize(url);
      const safeServiceUrl = sanitize(serviceUrl);

      const formats = {
        plain: () =>
          `${safeTitle}\n\n${separator}\n${safeText}\n${separator}\n\nSource: ${safeUrl}` +
          (includeLink ? `\nReadable: ${safeServiceUrl}` : ""),
        markdown: () =>
          `# ${safeTitle}\n\n> ${safeText.replace(
            /\n/g,
            "\n> "
          )}\n\n[Source](${safeUrl})` +
          (includeLink ? `\n\n[Readable](${safeServiceUrl})` : ""),
        html: () =>
          `<blockquote><h2>${safeTitle}</h2><p>${safeText.replace(
            /\n/g,
            "<br>"
          )}</p><footer><a href="${safeUrl}">Source</a>` +
          (includeLink
            ? ` | <a href="${safeServiceUrl}">Readable</a></footer></blockquote>`
            : "</footer></blockquote>"),
        twitter: () =>
          `"${
            safeText.length > 240
              ? safeText.substring(0, 240) + "..."
              : safeText
          }"\n\n${safeUrl}`,
        academic: () =>
          `${safeTitle}. Retrieved from ${safeUrl} on ${new Date().toLocaleDateString()}`,
        default: () =>
          `*${safeTitle}*\n\n${separator}\n${safeText}\n${separator}\n\n- Source: ${safeUrl}` +
          (includeLink ? `\n- Readable: ${safeServiceUrl}` : ""),
      };
      return (formats[format] || formats.default)();
    },

    share: {
      whatsapp: (text) =>
        window.open(
          `https://wa.me/?text=${encodeURIComponent(text)}`,
          "_blank"
        ),
      twitter: (text) =>
        window.open(
          `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`,
          "_blank"
        ),
      email: (text, title) =>
        window.open(
          `mailto:?subject=${encodeURIComponent(
            title
          )}&body=${encodeURIComponent(text)}`,
          "_blank"
        ),
      qrCode: (text) =>
        window.open(
          `https://quickchart.io/qr?text=${encodeURIComponent(text)}&size=300`,
          "_blank"
        ),
    },
  };

  // UI module
  const UI = {
    createStyles: (shadowRoot) => {
      const style = document.createElement("style");
      const cssText = `
         :host { 
           position: fixed; 
           top: 10px; 
           right: 10px; /* antes 10px */
           z-index: 999999; 
         }
         
         .citation-tool { 
           width: 300px; 
           background: #fff; 
           border: 1px solid #ddd; 
           border-radius: 10px; 
           font-family: Roboto, Helvetica, Arial, sans-serif; 
           box-shadow: 0 2px 10px rgba(0,0,0,0.1); 
         }
         
         .citation-tool a { 
           text-decoration: none; 
         }
         
         .citation-tool a:hover { 
           text-decoration: underline; 
         }
 
         #citation-tool-host {
          resize: none;
          width: 300px !important;
          min-width: 300px !important;
          max-width: 300px !important;
        }
 
        @media (max-width: 300px) {
          #citation-tool-host {
            width: 95% !important;
            /* min-width: 90% !important; */
            max-width: 95% !important;
          }
        }
 
         .citation-header { 
           display: flex; 
           justify-content: space-between; 
           align-items: center; 
           background: #296fa7; 
           color: white; 
           border-top-left-radius: 10px;
           border-top-right-radius: 10px;
           overflow: hidden;
         }
         
         .citation-drag-handle {
           flex-grow: 3; /* antes 1 */
           padding: 10px 10px; /* antes 10px 18px */
           font-size: 1.1em;
           cursor: move;
           user-select: none;
           -webkit-user-select: none;
           position: relative;
         }
         
         .citation-drag-handle::after {
           content: "";
           position: absolute;
           top: 0;
           left: 0;
           right: 0;
           bottom: 0;
           background: rgba(255, 255, 255, 0.05);
           pointer-events: none;
           opacity: 0;
           transition: opacity 0.2s;
         }
         
         .citation-drag-handle:hover::after {
           opacity: 1;
         }
         
         .citation-drag-handle.dragging::after {
           opacity: 1;
           background: rgba(255, 255, 255, 0.1);
         }
         
         .window-controls { 
           display: flex; 
           align-items: center;
           padding-right: 5px; /* antes 10px */
         }
         
         .window-controls button { 
           background: none; 
           border: none; 
           color: white; 
           cursor: pointer; 
           font-size: 1.2em; 
           padding: 10px; 
           min-width: 44px; 
           min-height: 44px; 
           text-align: center; 
           line-height: 1; 
           margin: 0 2px;
           border-radius: 4px;
           position: relative;
           z-index: 2;
         }
         
         .window-controls button:hover,
         .window-controls button:focus {
           background-color: rgba(255, 255, 255, 0.2);
         }
         
         .window-controls button:active {
           background-color: rgba(255, 255, 255, 0.3);
         }
         
         .window-controls .minimize-btn { 
           margin-right: 5px; 
         }
         
         .citation-container { 
           padding: 12px; 
           max-height: 70vh; 
           overflow-y: auto; 
         }
         
         .citation-preview { 
           padding: 8px; 
           margin: 8px 0; 
           border: 1px solid #eee; 
           border-radius: 4px; 
           max-height: 120px; 
           overflow-y: auto; 
           white-space: pre-wrap; 
           font-size: 13px; 
         }
         
         .citation-button { 
           width: 100%; 
           padding: 8px; 
           margin: 4px 0; 
           border: none; 
           border-radius: 4px; 
           background: #4CAF50;
           color: white; 
           cursor: pointer; 
           font-size: 14px;
         }
         
         .citation-button.whatsapp { 
           background: #2196F3; 
         }
         
         .citation-button.twitter { 
           background: #2196F3; 
         }
         
         .citation-button.email { 
           background: #2196F3; 
         }
         
         .citation-button.readability { 
           background: #EA4335; 
         }
         
         .citation-button.clear { 
           background: #EA4335; 
         }
         
         .citation-button.qr { 
           background: #4CAF50; 
         }
         
         .citation-tool.minimized { 
           height: auto !important; 
         }
         
         .citation-tool.minimized .citation-container { 
           display: none; 
         }
         
         select { 
           width: 100%; 
           padding: 6px; 
           margin: 4px 0; 
           border: 1px solid #ddd; 
           border-radius: 4px; 
           box-sizing: border-box; 
           font-size: 14px;
         }
         
         input[type="checkbox"] { 
           display: inline-flex; 
           padding: 6px; 
           margin: 0 8px; 
         }
         
         label { 
           font-size: 1em; 
           display: inline-flex; 
           padding: 6px; 
           align-items: center; 
         }
         
         .mode-selector { 
           margin-bottom: 8px; 
         }
         
         .mode-buttons { 
           display: flex; 
           margin: 8px 0; 
           border-radius: 4px; 
           overflow: hidden;
           border: 1px solid #ddd;
         }
         
         .mode-buttons button { 
           flex: 1; 
           padding: 6px; 
           border: none;
           background: #f5f5f5;
           cursor: pointer;
         }
         
         .mode-buttons button.active { 
           background: #296fa7; 
           color: white; 
         }
         
         .citation-footer { 
           padding: 8px; 
           text-align: center; 
           font-size: 12px; 
           display: flex; 
           justify-content: center; 
           align-items: center; 
           gap: 4px; 
           flex-wrap: wrap; 
           border-bottom-left-radius: 10px;
           border-bottom-right-radius: 10px;
           background: #444;
           color: #ddd;
         }
 
         .citation-footer a {
           color: #ddd; /*  #64B5F6; */
         }
         
         .clipboard-controls { 
           margin: 8px 0; 
         }
         
         #citation-feedback {
           position: fixed;
           bottom: 20px;
           left: 50%;
           transform: translateX(-50%);
           background: rgba(0,0,0,0.8);
           color: white;
           padding: 10px 20px;
           border-radius: 5px;
           z-index: 10000;
           opacity: 0;
           transition: opacity 0.3s;
           pointer-events: none;
         }
         
         /* Melhorias para responsividade em dispositivos móveis */
         @media (max-width: 300px) {
           .citation-tool {
             width: 95%; /* antes 90% */
             max-width: 300px;
           }
           
           .citation-button {
             padding: 12px 8px; /* Botões maiores para facilitar o toque */
           }
           
           .window-controls button {
             min-width: 48px; /* Área de toque ainda maior em mobile */
             min-height: 48px;
             margin: 0 3px;
           }
           
           .citation-drag-handle {
             padding: 15px 18px; /* Área de drag maior em mobile */
           }
         }
         
         @media (prefers-color-scheme: dark) {
           .citation-tool { 
             background: #333; 
             color: #fff; 
             border-color: #444; 
           }
           
           .citation-header { 
             background: #1a4a73; 
           }
           
           .citation-preview { 
             background: #222; 
             border-color: #444; 
           }
           
           select, input { 
             background: #444; 
             color: #fff; 
             border-color: #555; 
           }
           
           .mode-buttons button { 
             background: #444;
             color: #fff;
           }
         }
       `;
      style.appendChild(document.createTextNode(cssText));
      shadowRoot.appendChild(style);
    },

    createUI: (shadowRoot, hostElement) => {
      const { pageUrl, pageTitle, selectedText } = {
        pageUrl: location.href,
        pageTitle: document.title || "Untitled",
        selectedText: Utils.getPageContent(),
      };

      const badge = document.createElement("div");
      badge.className = "citation-tool";
      badge.id = CONFIG.BADGE_ID;

      // Cabeçalho com separação estrutural entre área arrastável e controles
      const header = document.createElement("div");
      header.className = "citation-header";

      // Área exclusiva para arrastar (drag handle)
      const dragHandle = document.createElement("div");
      dragHandle.className = "citation-drag-handle";
      dragHandle.setAttribute("data-role", "drag-area");

      const titleElement = document.createElement("h3");
      titleElement.textContent = CONFIG.APP_INFO.name;
      titleElement.style.margin = "0";
      dragHandle.appendChild(titleElement);

      // Área de controles separada
      const controls = document.createElement("div");
      controls.className = "window-controls";
      controls.setAttribute("data-role", "controls");

      const minimizeBtn = document.createElement("button");
      minimizeBtn.className = "minimize-btn";
      minimizeBtn.textContent = "−";
      minimizeBtn.setAttribute("data-action", "minimize");
      minimizeBtn.setAttribute("aria-label", "Minimize");

      const closeBtn = document.createElement("button");
      closeBtn.textContent = "×";
      closeBtn.setAttribute("data-action", "close");
      closeBtn.setAttribute("aria-label", "Close");

      controls.appendChild(minimizeBtn);
      controls.appendChild(closeBtn);

      // Montagem do cabeçalho com as duas áreas separadas
      header.appendChild(dragHandle);
      header.appendChild(controls);

      const container = document.createElement("div");
      container.className = "citation-container";

      // Mode selector
      const modeSelector = document.createElement("div");
      modeSelector.className = "mode-selector";
      const modeLabel = document.createElement("label");
      modeLabel.textContent = "Capture Mode:";
      modeSelector.appendChild(modeLabel);

      const modeButtons = document.createElement("div");
      modeButtons.className = "mode-buttons";

      const selectionBtn = document.createElement("button");
      selectionBtn.textContent = "Selection";
      selectionBtn.dataset.mode = "selection";
      if (state.captureMode === "selection")
        selectionBtn.classList.add("active");

      const clipboardBtn = document.createElement("button");
      clipboardBtn.textContent = "Clipboard";
      clipboardBtn.dataset.mode = "clipboard";
      if (state.captureMode === "clipboard")
        clipboardBtn.classList.add("active");

      modeButtons.appendChild(selectionBtn);
      modeButtons.appendChild(clipboardBtn);
      modeSelector.appendChild(modeButtons);

      const preview = document.createElement("div");
      preview.className = "citation-preview";
      preview.textContent = selectedText;

      const clipboardControls = document.createElement("div");
      clipboardControls.className = "clipboard-controls";
      clipboardControls.style.display =
        state.captureMode === "clipboard" ? "block" : "none";

      const addClipboardBtn = document.createElement("button");
      addClipboardBtn.className = "citation-button";
      addClipboardBtn.textContent = "Add from Clipboard";
      addClipboardBtn.id = "add-clipboard";

      const clearClipboardBtn = document.createElement("button");
      clearClipboardBtn.className = "citation-button clear";
      clearClipboardBtn.textContent = "Clear Collection";
      clearClipboardBtn.id = "clear-clipboard";

      clipboardControls.appendChild(addClipboardBtn);
      clipboardControls.appendChild(clearClipboardBtn);

      // Format controls
      const formatLabel = document.createElement("label");
      formatLabel.textContent = "Format:";
      const formatSelect = document.createElement("select");
      formatSelect.id = "format-select";
      CONFIG.FORMATS.forEach((f) => {
        const option = document.createElement("option");
        option.value = f.value;
        option.textContent = f.text;
        formatSelect.appendChild(option);
      });

      // Readability controls - modificado conforme solicitado
      const readabilityContainer = document.createElement("div");

      const readabilityCheck = document.createElement("input");
      readabilityCheck.type = "checkbox";
      readabilityCheck.id = "readability-check";
      readabilityCheck.checked = state.readabilityEnabled;

      const readabilityLabel = document.createElement("label");
      readabilityLabel.htmlFor = "readability-check";
      readabilityLabel.textContent = "Readability Link";

      // Adiciona o checkbox antes do label
      readabilityContainer.appendChild(readabilityLabel);
      readabilityContainer.appendChild(readabilityCheck);

      const readabilitySelect = document.createElement("select");
      readabilitySelect.id = "readability-select";
      CONFIG.READABILITY_SERVICES.forEach((s, i) => {
        const option = document.createElement("option");
        option.value = i;
        option.textContent = s.name;
        readabilitySelect.appendChild(option);
      });
      readabilitySelect.style.display = state.readabilityEnabled
        ? "block"
        : "none";

      // Action buttons
      const copyBtn = document.createElement("button");
      copyBtn.className = "citation-button";
      copyBtn.textContent = "Copy to Clipboard";
      copyBtn.id = "copy-button";

      const whatsappBtn = document.createElement("button");
      whatsappBtn.className = "citation-button whatsapp";
      whatsappBtn.textContent = "Share on WhatsApp";
      whatsappBtn.id = "whatsapp-button";

      const twitterBtn = document.createElement("button");
      twitterBtn.className = "citation-button twitter";
      twitterBtn.textContent = "Share on Twitter/X";
      twitterBtn.id = "twitter-button";

      const emailBtn = document.createElement("button");
      emailBtn.className = "citation-button email";
      emailBtn.textContent = "Share via Email";
      emailBtn.id = "email-button";

      const readabilityBtn = document.createElement("button");
      readabilityBtn.className = "citation-button readability";
      readabilityBtn.textContent = "View in Readability Service";
      readabilityBtn.id = "readability-button";

      const qrBtn = document.createElement("button");
      qrBtn.className = "citation-button qr";
      qrBtn.textContent = "Scan the QR Code";
      qrBtn.id = "qr-button";

      // Footer
      const footer = document.createElement("div");
      footer.className = "citation-footer";

      const versionSpan = document.createElement("span");
      versionSpan.textContent = CONFIG.APP_INFO.version;
      footer.appendChild(versionSpan);
      footer.appendChild(document.createTextNode("|"));

      const creditsLink = document.createElement("a");
      creditsLink.href = "https://linktr.ee/magasine";
      creditsLink.target = "_blank";
      creditsLink.textContent = "by @magasine";
      footer.appendChild(creditsLink);
      footer.appendChild(document.createTextNode("|"));

      const helpLink = document.createElement("a");
      helpLink.href =
        "https://drive.google.com/file/d/1PZcw-Syb1ngz3fudr15LPn5Civqzrnzz/view?usp=sharing";
      helpLink.target = "_blank";
      helpLink.textContent = "Help";
      footer.appendChild(helpLink);

      // Assemble UI
      container.appendChild(modeSelector);
      container.appendChild(preview);
      container.appendChild(clipboardControls);
      container.appendChild(formatLabel);
      container.appendChild(formatSelect);
      container.appendChild(readabilityContainer); // Adiciona o container com checkbox e label
      container.appendChild(readabilitySelect); // Adiciona o seletor
      container.appendChild(readabilityBtn);
      container.appendChild(copyBtn);
      container.appendChild(qrBtn);
      container.appendChild(whatsappBtn);
      container.appendChild(twitterBtn);
      container.appendChild(emailBtn);

      badge.appendChild(header);
      badge.appendChild(container);
      badge.appendChild(footer);

      // Event handlers
      const updatePreview = () => {
        clipboardControls.style.display =
          state.captureMode === "clipboard" ? "block" : "none";
        if (state.captureMode === "clipboard") {
          preview.textContent = state.clipboardItems.length
            ? state.clipboardItems
                .map(
                  (item, i) =>
                    `[${i + 1}] ${
                      item.length > 100 ? item.substring(0, 100) + "..." : item
                    }\n${CONFIG.CLIPBOARD_SEPARATOR}`
                )
                .join("")
            : "No clipboard items added yet.";
        } else {
          preview.textContent = Utils.getPageContent();
        }
      };

      [selectionBtn, clipboardBtn].forEach((btn) => {
        btn.addEventListener("click", () => {
          state.captureMode = btn.dataset.mode;
          selectionBtn.classList.toggle(
            "active",
            state.captureMode === "selection"
          );
          clipboardBtn.classList.toggle(
            "active",
            state.captureMode === "clipboard"
          );
          updatePreview();
        });
      });

      addClipboardBtn.addEventListener("click", async () => {
        const text = await Utils.getClipboardText();
        if (text && !text.includes("Clipboard access")) {
          state.clipboardItems.push(text);
          updatePreview();
          Utils.showFeedback(shadowRoot, "✓ Added to collection!");
        } else {
          Utils.showFeedback(shadowRoot, "✗ Could not access clipboard");
        }
      });

      clearClipboardBtn.addEventListener("click", () => {
        state.clipboardItems = [];
        updatePreview();
        Utils.showFeedback(shadowRoot, "✓ Collection cleared");
      });

      const getFormattedText = () => {
        const format = formatSelect.value;
        const service = CONFIG.READABILITY_SERVICES[readabilitySelect.value];
        const includeLink = state.readabilityEnabled; // Usa o estado do checkbox
        const text =
          state.captureMode === "selection"
            ? preview.textContent
            : state.clipboardItems.join(CONFIG.CLIPBOARD_SEPARATOR);
        return Citation.format(
          format,
          text,
          pageTitle,
          pageUrl,
          service,
          includeLink
        );
      };

      copyBtn.addEventListener("click", async () => {
        const success = await Utils.copyToClipboard(getFormattedText());
        Utils.showFeedback(shadowRoot, success ? "✓ Copied!" : "✗ Copy failed");
      });

      whatsappBtn.addEventListener("click", () =>
        Citation.share.whatsapp(getFormattedText())
      );

      twitterBtn.addEventListener("click", () =>
        Citation.share.twitter(getFormattedText())
      );

      emailBtn.addEventListener("click", () =>
        Citation.share.email(getFormattedText(), pageTitle)
      );

      readabilityBtn.addEventListener("click", () => {
        const service = CONFIG.READABILITY_SERVICES[readabilitySelect.value];
        window.open(service.url(pageUrl), "_blank");
      });

      qrBtn.addEventListener("click", () =>
        Citation.share.qrCode(getFormattedText())
      );

      // Evento para o checkbox de readability
      readabilityCheck.addEventListener("change", (e) => {
        state.readabilityEnabled = e.target.checked;
        readabilitySelect.style.display = state.readabilityEnabled
          ? "block"
          : "none";
      });

      // Eventos específicos para os botões de controle
      minimizeBtn.addEventListener("click", (e) => {
        state.isMinimized = !state.isMinimized;
        badge.classList.toggle("minimized", state.isMinimized);
        minimizeBtn.textContent = state.isMinimized ? "+" : "−";
        Utils.showFeedback(
          shadowRoot,
          state.isMinimized ? "UI minimized" : "UI restored"
        );
      });

      closeBtn.addEventListener("click", () => {
        hostElement.remove();
      });

      document.addEventListener("selectionchange", () => {
        if (state.captureMode === "selection") {
          setTimeout(() => {
            const selection = window.getSelection().toString().trim();
            if (selection) preview.textContent = sanitize(selection);
          }, 300);
        }
      });

      return {
        badge,
        dragHandle,
      };
    },
  };

  // Implementação de drag melhorada para funcionar em touch
  const setupDrag = (element, dragHandle) => {
    let pos1 = 0,
      pos2 = 0,
      pos3 = 0,
      pos4 = 0;

    // Armazenar a largura original antes de iniciar o arrasto
    let originalWidth = element.style.width;

    // Função para iniciar o arrasto
    const startDrag = (clientX, clientY) => {
      state.isDragging = true;
      dragHandle.classList.add("dragging");

      // Manter a largura original durante o arrasto
      originalWidth = window.getComputedStyle(element).width;
      element.style.width = originalWidth;
      element.style.minWidth = originalWidth;
      element.style.maxWidth = originalWidth;

      pos3 = clientX;
      pos4 = clientY;
    };

    // Função para mover o elemento
    const moveDrag = (clientX, clientY) => {
      if (!state.isDragging) return;

      pos1 = pos3 - clientX;
      pos2 = pos4 - clientY;
      pos3 = clientX;
      pos4 = clientY;

      let newTop = element.offsetTop - pos2;
      let newLeft = element.offsetLeft - pos1;
      const hostRect = element.getBoundingClientRect();

      // Limites para manter o elemento dentro da janela
      if (newTop < 0) newTop = 0;
      if (newLeft < 0) newLeft = 0;
      if (newTop + hostRect.height > window.innerHeight)
        newTop = window.innerHeight - hostRect.height;
      if (newLeft + hostRect.width > window.innerWidth)
        newLeft = window.innerWidth - hostRect.width;

      // Aplicar nova posição mantendo a largura fixa
      element.style.top = newTop + "px";
      element.style.left = newLeft + "px";
      element.style.width = originalWidth;
      element.style.minWidth = originalWidth;
      element.style.maxWidth = originalWidth;
    };

    // Função para finalizar o arrasto
    const endDrag = () => {
      if (!state.isDragging) return;

      state.isDragging = false;
      dragHandle.classList.remove("dragging");
      Utils.debug("Drag finalizado");
    };

    // Handlers para eventos de mouse
    const handleMouseDown = (e) => {
      e.preventDefault();
      startDrag(e.clientX, e.clientY);
      document.addEventListener("mouseup", handleMouseUp);
      document.addEventListener("mousemove", handleMouseMove);
    };

    const handleMouseMove = (e) => {
      e.preventDefault();
      moveDrag(e.clientX, e.clientY);
    };

    const handleMouseUp = () => {
      endDrag();
      document.removeEventListener("mouseup", handleMouseUp);
      document.removeEventListener("mousemove", handleMouseMove);
    };

    // Handlers para eventos de touch
    const handleTouchStart = (e) => {
      // Verificar se o toque foi na área de drag
      if (e.touches.length === 1) {
        const touch = e.touches[0];
        startDrag(touch.clientX, touch.clientY);

        // Importante: Não usar preventDefault aqui para permitir outros eventos touch

        document.addEventListener("touchend", handleTouchEnd, {
          passive: false,
        });
        document.addEventListener("touchcancel", handleTouchEnd, {
          passive: false,
        });
        document.addEventListener("touchmove", handleTouchMove, {
          passive: false,
        });
      }
    };

    const handleTouchMove = (e) => {
      if (e.touches.length === 1 && state.isDragging) {
        e.preventDefault(); // Prevenir scroll apenas durante o arrasto
        const touch = e.touches[0];
        moveDrag(touch.clientX, touch.clientY);
      }
    };

    const handleTouchEnd = (e) => {
      if (state.isDragging) {
        e.preventDefault();
      }
      endDrag();
      document.removeEventListener("touchend", handleTouchEnd);
      document.removeEventListener("touchcancel", handleTouchEnd);
      document.removeEventListener("touchmove", handleTouchMove);
    };

    // Adiciona os event listeners à área de drag
    dragHandle.addEventListener("mousedown", handleMouseDown);

    // Importante: usar { passive: false } para permitir preventDefault em touchmove
    dragHandle.addEventListener("touchstart", handleTouchStart, {
      passive: true,
    });

    // Retorna uma função para remover os event listeners se necessário
    return () => {
      dragHandle.removeEventListener("mousedown", handleMouseDown);
      dragHandle.removeEventListener("touchstart", handleTouchStart);
      document.removeEventListener("mouseup", handleMouseUp);
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("touchend", handleTouchEnd);
      document.removeEventListener("touchcancel", handleTouchEnd);
      document.removeEventListener("touchmove", handleTouchMove);
    };
  };

  // Initialization
  const init = () => {
    console.log(
      `[${CONFIG.APP_INFO.name} DEBUG] init() chamada. CONFIG.HOST_ID: ${CONFIG.HOST_ID}, Versão do script: ${CONFIG.APP_INFO.version}`
    );

    const existingHost = document.getElementById(CONFIG.HOST_ID);

    if (existingHost) {
      console.log(
        `[${CONFIG.APP_INFO.name} DEBUG] Host existente encontrado:`,
        existingHost
      );
      try {
        existingHost.remove();
        console.log(
          `[${CONFIG.APP_INFO.name} DEBUG] existingHost.remove() chamado. UI foi fechada.`
        );
        if (document.getElementById(CONFIG.HOST_ID)) {
          console.error(
            `[${CONFIG.APP_INFO.name} DEBUG] ERRO CRÍTICO: Host ainda existe no DOM após remove()! ID: ${CONFIG.HOST_ID}`
          );
        } else {
          console.log(
            `[${CONFIG.APP_INFO.name} DEBUG] SUCESSO: Host não foi encontrado no DOM após remove(). Comportamento de toggle: UI fechada.`
          );
        }
        return;
      } catch (e) {
        console.error(
          `[${CONFIG.APP_INFO.name} DEBUG] Erro ao tentar remover existingHost:`,
          e
        );
        return;
      }
    } else {
      console.log(
        `[${CONFIG.APP_INFO.name} DEBUG] Nenhum host existente encontrado com ID: ${CONFIG.HOST_ID}. Criando um novo para abrir a UI.`
      );
    }

    // Create Shadow DOM host
    const hostElement = document.createElement("div");
    hostElement.id = CONFIG.HOST_ID;
    console.log(
      `[${CONFIG.APP_INFO.name} DEBUG] Novo hostElement criado com ID: ${hostElement.id}`
    );
    document.body.appendChild(hostElement);

    // Initialize Shadow DOM
    const shadow = hostElement.attachShadow({ mode: "open" });
    UI.createStyles(shadow);
    const { badge, dragHandle } = UI.createUI(shadow, hostElement);
    shadow.appendChild(badge);

    // Setup drag functionality apenas na área de drag
    setupDrag(hostElement, dragHandle);

    // Ajusta posição inicial para dispositivos móveis
    if (Utils.isMobileDevice()) {
      hostElement.style.top = "10px"; // antes 50px - Posição inicial mais baixa em dispositivos móveis
      hostElement.style.right = "5px"; // antes 10px
    }

    console.log(
      `[${CONFIG.APP_INFO.name} DEBUG] Nova UI criada e configurada no host ${hostElement.id}.`
    );
  };

  // Prevent multiple executions
  if (
    window.citationToolInitiated &&
    Date.now() - window.citationToolInitiated < 1000
  ) {
    console.warn(
      `[${CONFIG.APP_INFO.name}] Script chamado novamente em menos de 1 segundo. Ignorando esta chamada para evitar duplicação rápida.`
    );
    return;
  }
  window.citationToolInitiated = Date.now();

  console.log(
    `[${CONFIG.APP_INFO.name} DEBUG] Script principal carregado (Versão: ${CONFIG.APP_INFO.version}).`
  );
  init();
})();

GitHub Events

Total
  • Push event: 26
  • Create event: 2
Last Year
  • Push event: 26
  • Create event: 2