├── src ├── style.css ├── background.ts ├── lib │ ├── constants.ts │ ├── preventEnterOnIME.ts │ └── utils.ts ├── components │ └── shieldIcon.tsx ├── content.ts ├── options.tsx └── popup.tsx ├── assets ├── icon.png └── icon-min.png ├── postcss.config.js ├── tsconfig.json ├── .prettierrc.cjs ├── .gitignore ├── tailwind.config.js ├── .github └── workflows │ └── submit.yml ├── package.json └── README.md /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawamataryo/ime-submit-blocker/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/icon-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawamataryo/ime-submit-blocker/HEAD/assets/icon-min.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('postcss').ProcessOptions} 3 | */ 4 | module.exports = { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": [ 4 | "node_modules" 5 | ], 6 | "include": [ 7 | ".plasmo/index.d.ts", 8 | "./**/*.ts", 9 | "./**/*.tsx" 10 | ], 11 | "compilerOptions": { 12 | "paths": { 13 | "~*": [ 14 | "./src/*" 15 | ] 16 | }, 17 | "baseUrl": "." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { sendToContentScript } from "@plasmohq/messaging" 2 | import { INITIALIZE_SUBMIT_BLOCK_MESSAGE_NAME } from "~lib/constants" 3 | 4 | // SPAでのページ遷移への対応 5 | (() => { 6 | chrome.history.onVisited.addListener( 7 | async () => { 8 | await sendToContentScript({ 9 | name: INITIALIZE_SUBMIT_BLOCK_MESSAGE_NAME, 10 | }) 11 | } 12 | ) 13 | })() 14 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BEHAVIOR_STORAGE_KEY = "ime_submit_blocker_default_behavior" 2 | 3 | export const BLACK_LIST_STORAGE_KEY = "ime_submit_blocker_black_list" 4 | 5 | export const WAIT_TIME_STORAGE_KEY = "ime_submit_blocker_wait_time" 6 | 7 | export const TOGGLE_IME_SUBMIT_BLOCK_MESSAGE_NAME = "toggle-ime-submit-block" 8 | 9 | export const INITIALIZE_SUBMIT_BLOCK_MESSAGE_NAME = "initialize-submit-block" 10 | 11 | export const DEFAULT_WAIT_TIME = 2500 12 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | module.exports = { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: [require.resolve("@plasmohq/prettier-plugin-sort-imports")], 14 | importOrder: ["^@plasmohq/(.*)$", "^~(.*)$", "^[./]"], 15 | importOrderSeparation: true, 16 | importOrderSortSpecifiers: true 17 | } 18 | -------------------------------------------------------------------------------- /src/components/shieldIcon.tsx: -------------------------------------------------------------------------------- 1 | const ShieldIcon = () => ( 2 | 8 | 15 | 16 | ) 17 | 18 | export default ShieldIcon 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | #cache 13 | .turbo 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | 25 | # local env files 26 | .env* 27 | 28 | out/ 29 | build/ 30 | dist/ 31 | 32 | # plasmo - https://www.plasmo.com 33 | .plasmo 34 | 35 | # bpp - http://bpp.browser.market/ 36 | keys.json 37 | 38 | # typescript 39 | .tsbuildinfo 40 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "jit", 4 | darkMode: "class", 5 | content: ["./**/*.tsx"], 6 | plugins: [require("daisyui")], 7 | daisyui: { 8 | themes: [ 9 | { 10 | winter: { 11 | ...require("daisyui/src/colors/themes")["[data-theme=winter]"], 12 | primary: "#00B4DB" 13 | } 14 | }, 15 | { 16 | night: { 17 | ...require("daisyui/src/colors/themes")["[data-theme=night]"], 18 | primary: "#00B4DB" 19 | } 20 | } 21 | ], 22 | darkTheme: "night" 23 | }, 24 | extend: { 25 | colors: { 26 | primary: "#01AED6" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build the extension 27 | run: pnpm build 28 | - name: Package the extension into a zip artifact 29 | run: pnpm package 30 | - name: Browser Platform Publish 31 | uses: PlasmoHQ/bpp@v3 32 | with: 33 | keys: ${{ secrets.SUBMIT_KEYS }} 34 | artifact: build/chrome-mv3-prod.zip 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ime-submit-blocker", 3 | "displayName": "IME Submit Blocker", 4 | "version": "1.2.0", 5 | "description": "IMEでの文字変換確定時のEnterでフォームが送信されることを防ぐ拡張機能", 6 | "author": "kawamataryo", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "package": "plasmo package" 11 | }, 12 | "dependencies": { 13 | "@plasmohq/messaging": "^0.1.7", 14 | "@plasmohq/storage": "^1.4.0", 15 | "daisyui": "^2.51.5", 16 | "plasmo": "0.67.4", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0", 19 | "zod": "^3.21.4" 20 | }, 21 | "devDependencies": { 22 | "@plasmohq/prettier-plugin-sort-imports": "3.6.3", 23 | "@types/chrome": "0.0.228", 24 | "@types/node": "18.15.11", 25 | "@types/react": "18.0.33", 26 | "@types/react-dom": "18.0.11", 27 | "autoprefixer": "^10.4.14", 28 | "postcss": "^8.4.21", 29 | "prettier": "2.8.7", 30 | "tailwindcss": "^3.3.1", 31 | "typescript": "5.0.3" 32 | }, 33 | "manifest": { 34 | "host_permissions": [ 35 | "https://*/*" 36 | ], 37 | "permissions": [ 38 | "tabs", 39 | "storage", 40 | "history" 41 | ] 42 | }, 43 | "volta": { 44 | "node": "16.20.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/preventEnterOnIME.ts: -------------------------------------------------------------------------------- 1 | export const preventEnterOnIME = ( 2 | dom: Document | ShadowRoot, 3 | signal: AbortSignal 4 | ) => { 5 | const webComponents = Array.from(dom.querySelectorAll("*")).filter((d) => 6 | d.tagName.includes("-") 7 | ) 8 | 9 | let lastKeyDownIsIME = false 10 | 11 | webComponents.forEach((d) => { 12 | if (d.shadowRoot) { 13 | preventEnterOnIME(d.shadowRoot, signal) 14 | } 15 | }) 16 | 17 | dom.addEventListener( 18 | "keydown", 19 | (e: KeyboardEvent) => { 20 | // textarea, input, role="textbox" 以外の場合は除外 21 | if (!( 22 | e.target instanceof HTMLTextAreaElement || 23 | e.target instanceof HTMLInputElement || 24 | (e.target as Element).getAttribute('role') === 'textbox' 25 | )) return 26 | 27 | if (e.key === "Enter") { 28 | if (e.isComposing) { 29 | e.preventDefault() 30 | e.stopPropagation() 31 | lastKeyDownIsIME = true 32 | } else { 33 | lastKeyDownIsIME = false 34 | } 35 | } 36 | }, 37 | { 38 | capture: true, 39 | signal 40 | } 41 | ) 42 | 43 | dom.addEventListener( 44 | "keyup", 45 | (e: KeyboardEvent) => { 46 | if (!(e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement)) return 47 | if (e.key === "Enter" && lastKeyDownIsIME) { 48 | e.preventDefault() 49 | e.stopPropagation() 50 | lastKeyDownIsIME = false 51 | } 52 | }, 53 | { 54 | capture: true, 55 | signal 56 | } 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INITIALIZE_SUBMIT_BLOCK_MESSAGE_NAME, 3 | TOGGLE_IME_SUBMIT_BLOCK_MESSAGE_NAME 4 | } from "~lib/constants" 5 | import { preventEnterOnIME } from "~lib/preventEnterOnIME" 6 | import { isAutoApply, isBlackListed, log, wait } from "~lib/utils" 7 | 8 | let abortController: AbortController | null = null 9 | 10 | const enableBlocker = () => { 11 | abortController?.abort() 12 | abortController = new AbortController() 13 | const signal = abortController.signal 14 | preventEnterOnIME(document, signal) 15 | log("[IMESubmitBlocker] Enabled IME submit block 🚀") 16 | } 17 | 18 | const disableBlocker = () => { 19 | abortController?.abort() 20 | log("[IMESubmitBlocker] Disabled IME submit block 🗑️") 21 | } 22 | 23 | const initializeBlocker = async () => { 24 | if (!(await isAutoApply())) { 25 | return 26 | } 27 | if (await isBlackListed()) { 28 | log("[IMESubmitBlocker] Current url is registered black list 📝") 29 | return 30 | } 31 | // 即時実行 32 | enableBlocker() 33 | await wait() 34 | // wait後に再実行(SPA対応) 35 | enableBlocker() 36 | } 37 | 38 | chrome.runtime.onMessage.addListener(async (message) => { 39 | switch (message.name) { 40 | case TOGGLE_IME_SUBMIT_BLOCK_MESSAGE_NAME: 41 | if (message.body.isBlocked) { 42 | enableBlocker() 43 | } else { 44 | disableBlocker() 45 | } 46 | break 47 | case INITIALIZE_SUBMIT_BLOCK_MESSAGE_NAME: 48 | await initializeBlocker() 49 | break 50 | default: 51 | throw new Error("Unknown message name") 52 | } 53 | }) 54 | ;(async () => { 55 | await initializeBlocker() 56 | })() 57 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { BLACK_LIST_STORAGE_KEY, DEFAULT_BEHAVIOR_STORAGE_KEY, DEFAULT_WAIT_TIME, WAIT_TIME_STORAGE_KEY } from "./constants" 2 | import { Storage } from "@plasmohq/storage" 3 | 4 | const storage = new Storage() 5 | 6 | export const isAutoApply = async () => { 7 | const defaultState = await storage.get( 8 | DEFAULT_BEHAVIOR_STORAGE_KEY 9 | ) 10 | if (defaultState === undefined) { 11 | storage.set(DEFAULT_BEHAVIOR_STORAGE_KEY, true) 12 | return true 13 | } 14 | return defaultState 15 | } 16 | 17 | export const isBlackListed = async () => { 18 | const storage = new Storage() 19 | const blackList = await storage.get(BLACK_LIST_STORAGE_KEY) || "" 20 | const parsedBlackList = blackList.split("\n") || [] 21 | const currentUrl = document.location.href 22 | return parsedBlackList.some((blackListedUrl) => { 23 | if (blackListedUrl === "") { 24 | return false 25 | } 26 | return currentUrl.startsWith(blackListedUrl) 27 | }) 28 | } 29 | 30 | export const getWaitTime = async () => { 31 | const waitTime = await storage.get(WAIT_TIME_STORAGE_KEY) 32 | if (waitTime === undefined) { 33 | storage.set(WAIT_TIME_STORAGE_KEY, DEFAULT_WAIT_TIME) 34 | return DEFAULT_WAIT_TIME 35 | } 36 | return waitTime 37 | } 38 | 39 | export const wait = () => new Promise(async (resolve) => { 40 | const waitTime = await getWaitTime() 41 | setTimeout(resolve, waitTime) 42 | }); 43 | 44 | export const log = (message: string) => { 45 | if(process.env.NODE_ENV === "development") { 46 | console.log(`ℹ️ [IMESubmitBlocker] ${message}`) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chrome web store version 5 | Edge add-on version 6 | Chrome web store rating 7 | Chrome Web Store users 8 | 9 | IMEでの入力変換確定時のEnterで誤送信されることを防ぐブラウザ拡張機能。 10 | 11 | https://user-images.githubusercontent.com/11070996/231881786-fc7711bf-771b-4e4b-a2fa-ffa14e23bcee.mp4 12 | 13 | ## 🌐 Install 14 | 15 | - [Chrome](https://chrome.google.com/webstore/detail/ime-submit-blocker/apmppndmejpolkldpeeipcejcbjfpblo?hl=ja&authuser=0) 16 | - [Edge](https://microsoftedge.microsoft.com/addons/detail/ime-submit-blocker/npdhgmdihnjdkicnokhncjjhmigafkld) 17 | 18 | ## ✨ Feature 19 | - 全サイトへの自動適応・手動適応 20 | - 除外サイトの設定 21 | - SPAへの対応 22 | 23 | ## 💕 Thanks 24 | 25 | - [Plasmo](https://www.plasmo.com/) 26 | - [userscript](https://gist.github.com/koseki/d377f8f2e6df6655a1e160a4e03421d1) 27 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import iconImageData from "data-base64:~assets/icon-min.png" 2 | 3 | import { useStorage } from "@plasmohq/storage/hook" 4 | 5 | import { 6 | BLACK_LIST_STORAGE_KEY, 7 | DEFAULT_BEHAVIOR_STORAGE_KEY, 8 | WAIT_TIME_STORAGE_KEY 9 | } from "~lib/constants" 10 | 11 | import "./style.css" 12 | 13 | 14 | function IndexOptions() { 15 | const [blackList, setBlackList] = useStorage( 16 | BLACK_LIST_STORAGE_KEY, 17 | "" 18 | ) 19 | const [defaultBehavior, setDefaultBehavior] = useStorage( 20 | DEFAULT_BEHAVIOR_STORAGE_KEY, 21 | true 22 | ) 23 | const [waitTime, setWaitTime] = useStorage( 24 | WAIT_TIME_STORAGE_KEY, 25 | 2500 26 | ) 27 | 28 | return ( 29 | <> 30 |
31 |

32 | 33 | IME Submit Blocker 34 |

35 |

36 | IMEでの日本語変換確定時のEnterでFormが送信されることを防ぐ拡張機能。※ 37 | 不具合、改善要望はGitHubの 38 | 41 | Issue 42 | 43 | へ。 44 |

45 |
46 | 47 |

48 | 自動で適応するかどうか。ONの場合は、Black 49 | Listに登録されているサイトを除き自動的に適応する。 50 |

51 | setDefaultBehavior(e.target.checked)} 56 | /> 57 |
58 |
59 | 60 |

61 | 自動適応の際にスクリプトの実行をどれくらい待つか。入力欄が遅延して表示されるサイトへの対応。 62 |

63 | setWaitTime(Number(e.target.value))}> 68 |
69 |
70 | 71 |

72 | {" "} 73 | 自動適応の際に除外するサイトの一覧。URLを改行区切りで入力。 74 |

75 | 80 |
81 |
82 |

83 |
84 | 92 | 93 | ) 94 | } 95 | 96 | export default IndexOptions 97 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | import "./style.css" 2 | 3 | import iconImageData from "data-base64:~assets/icon-min.png" 4 | import { useState } from "react" 5 | 6 | import { sendToContentScript } from "@plasmohq/messaging" 7 | import { Storage } from "@plasmohq/storage" 8 | 9 | import { 10 | BLACK_LIST_STORAGE_KEY, 11 | TOGGLE_IME_SUBMIT_BLOCK_MESSAGE_NAME 12 | } from "~lib/constants" 13 | 14 | function IndexPopup() { 15 | const toggleIMESubmitBlock = async (isBlocked: boolean) => { 16 | await sendToContentScript({ 17 | name: TOGGLE_IME_SUBMIT_BLOCK_MESSAGE_NAME, 18 | body: { 19 | isBlocked, 20 | shouldWait: false 21 | } 22 | }) 23 | } 24 | 25 | const openOptionPage = async () => { 26 | chrome.runtime.openOptionsPage() 27 | } 28 | 29 | const [notification, setNotification] = useState("") 30 | const showNotification = (message: string) => { 31 | setNotification(message) 32 | setTimeout(() => { 33 | setNotification("") 34 | }, 2500) 35 | } 36 | 37 | const addBlackList = async () => { 38 | const storage = new Storage() 39 | const blackList = (await storage.get(BLACK_LIST_STORAGE_KEY)) || "" 40 | chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { 41 | const currentUrl = new URL(tabs[0].url) 42 | if (blackList) { 43 | await storage.set( 44 | BLACK_LIST_STORAGE_KEY, 45 | `${blackList}\n${currentUrl.origin}` 46 | ) 47 | } else { 48 | await storage.set(BLACK_LIST_STORAGE_KEY, currentUrl.origin) 49 | } 50 | await toggleIMESubmitBlock(false) 51 | showNotification(`Added \n${currentUrl.origin}\n to Black List`) 52 | }) 53 | } 54 | 55 | return ( 56 |
57 |

58 | 59 | IME Submit Blocker 60 |

61 |
62 | 70 | 78 |
79 |
80 | 85 |

86 | 91 | 94 | 95 |

96 |
97 | {notification && ( 98 |
99 |
100 |

101 | 107 | 110 | 111 | {notification} 112 |

113 |
114 |
115 | )} 116 |
117 | ) 118 | } 119 | 120 | export default IndexPopup 121 | --------------------------------------------------------------------------------