3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張機能作成(サイドパネル) selectionchange

Last updated at Posted at 2023-12-30

Chrome拡張機能(Vite + React + TypeScript)

ダウンロード.gif

内容

ブラウザで選択された文字をサイドパネルに表示する

  • useStateを使いたい
  • JSON形式で値を受け取りたい

お試し簡易実装のため多少バグがあると思います。
ご了承ください。

開発環境

OS : macOS Sonoma 14.2.1
node : 18.15.0
npm : 9.8.1

環境構築

terminal
Viteで作成
> npm create vite@latest
> product name : productName (任意の名前)
> React
> TypeScript + SWC
> cd productName
> npm i 

ここで拡張機能に必要なものをインストール(* @crxjs/vite-pluginには@betaをつけてください)
> npm i -D @crxjs/vite-plugin@beta @types/chrome

> mkdir src/background
> touch src/background/background.ts

機能説明

chrome.scripting.executeScriptの中ではReactの関数など使用できないので注意。
background.tsを中継することでuseStateの値を更新できるようになります。

実装

manifest.jsonを作成させます。

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { crx, defineManifest } from "@crxjs/vite-plugin"

const manifest = defineManifest({
  manifest_version: 3,
  name: "chrome-extension", // 拡張機能の名前 
  version: "1.0.0",
  icons: { 
    // 128: "icon-128-active.png" // 拡張機能のアイコン
  },
  action: {
    default_popup: "index.html"
  },
  side_panel: {
    default_path: "index.html", // サイドパネルに表示するHTML
  },
  permissions: [ // 権限を付与
    'sidePanel',
    'tabs',
    'activeTab',
    'storage',
    'scripting'
  ],
  host_permissions: ["<all_urls>"], // 拡張機能が使えるURLを指定(今回は全てにしています)
  background: {
    service_worker: "src/background/background.ts"
  }
})

export default defineConfig((comand) => {
  return {
    server: {
      port: 5173,
      strictPort: true,
      hmr: {
        port: 5173
      },
    },
    build: {
      rollupOptions: {
        input: {

        },
        output: {

        },
      },
      watch: {

      }
    },
    plugins: [react(), crx({ manifest })],
  }
})
src/background/background.ts

// Chromeで更新があった場合に実行される
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // chrome://から始まるURLでは拡張機能は使えないみたいです
  if (tab.url?.startsWith("chrome://")) return

  if (changeInfo.status === "complete") {
    console.log(tabId, changeInfo, tab);
    send(JSON.stringify({ tabId: tabId, title: tab.title, update: true }))
  }
})

// Chromeのアクティブなタブが切り替わった際に実行される
chrome.tabs.onActivated.addListener((activeInfo) => {
  send(JSON.stringify({ ...activeInfo, update: true }))
})

// 拡張機能を実行した際、サイドパネルを開く
chrome.sidePanel
  .setPanelBehavior({ openPanelOnActionClick: true })
  .catch((error) => console.error(error))

// message受け取り用
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  let json = JSON.parse(message)
  if (json?.selectedText) {
    send(message)
  } else {
    send("background receive message [Success]")
  }
})


// message送信用
const send = (message: any) => {
  chrome.runtime.sendMessage(message)
}

App.tsx
import { useEffect, useState } from 'react'

function App() {
  const [firstInit, setFirstInit] = useState<boolean>(false)
  const [tabId, setTabId] = useState<number>(null!)
  const [tabTitle, setTabTitle] = useState<string>(null!)
  const [selectedText, setSelectedText] = useState<string>("")
  const [update, setUpdate] = useState<boolean>(false)

  useEffect(() => {
    init()
    chrome.runtime.onMessage.addListener(receiveMessage)
  }, [])

  // tabIdまたはtabTitleが変更された場合
  useEffect(() => {
    // Chromeタブが更新された際、再度selectionchangeイベントを実行する
    init()
    setUpdate(false)
  }, [!!firstInit && !!update ])

  // background.tsからmessageを受け取る
  const receiveMessage = (v: any) => {
    let json = JSON.parse(v)
    if (json?.title) { setTabTitle(json.title) }
    if (json?.selectedText) { setSelectedText(json.selectedText) }
    if (json?.update) { setUpdate(json.update) }
  }
  
  // Chromeタブ内でイベントを実行させる
  const init = async () => {
    setFirstInit(true)
    // Chromeタブの情報を取得
    let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    setTabId(tab.id!)
    setTabTitle(tab.title!)
    // Chromeタブに対してScriptを実行
    chrome.scripting.executeScript({
      target: { tabId: tab.id! },
      func: () => {
      
        /** -----------------------------------------
        * ここではReactの関数など使用できないので注意してください。
        ------------------------------------------- */
      
        // Chromeタブ内でのselectionchangeイベントを検知させます
        document.addEventListener("selectionchange", () => {
          let str = window.getSelection()?.toString()
          let json = { selectedText: str }
          // 検知した場合にから文字でなければbackground.ts側にmessageを送ります
          str && chrome.runtime.sendMessage(JSON.stringify(json))    
        })

      }
    })
  }

  return (
    <>
      <div>
        <div style={{ padding: "1rem" }}>
          <div>tabID:</div>
          <div style={{ background: "green" }}>&nbsp;{tabId}</div>
          <div>tabタイトル:</div>
          <div style={{ background: "rgb(0,120,154)" }}>&nbsp;{tabTitle}</div>
        </div>
        <div style={{ padding: "1rem" }}>
          <div>
            <textarea
              name=""
              id=""
              cols={30}
              rows={10}
              value={selectedText}
              onChange={(e) => setSelectedText(e.currentTarget.value)}
              style={{ width: "100%" }}
            />
          </div>
        </div>

      </div>
    </>
  )
}

export default App

拡張機能を実行してみる

Chrome拡張機能

npm run devまたはnpm run buildを実行し、distフォルダを作成しておいてください。

  • デベロッパー モードをONにします。
  • パッケージ化されていない拡張機能を読み込む
    • 今回作成したdistファイルを選択
    • Chromeの拡張機能に追加される
  • Chromeの拡張機能から実行

Chromeタブ内(chrome://以外)で選択された文字がサイドパネル上に表示されれば成功です。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?