feat: add form_saver

This commit is contained in:
Minhyeok Park 2024-10-04 13:12:03 +09:00
parent 1a7e70272c
commit e7b316be7a
Signed by: pmh_only
SSH Key Fingerprint: SHA256:g/OyGvi2pcd8ub9mqge/ohmDP0fZX/xOPWPIcM+9XpI
10 changed files with 1044 additions and 5 deletions

74
form_saver.ts Normal file
View File

@ -0,0 +1,74 @@
import { CommunicateMessage, FormSavedDataContent } from './types'
class FormSaverExtension {
private readonly api = browser ?? chrome
public startListen = (): void =>
this.api.runtime.onMessage.addListener(this.messageHandler.bind(this))
// ---
private readonly messageHandler = (message: unknown, _: any, sendResponse: (message: CommunicateMessage) => void): undefined => {
const receivedMessage = message as CommunicateMessage
if (receivedMessage.type === 'SAVE_FORM') {
return sendResponse(this.saveForm()) as undefined
}
if (receivedMessage.type === 'LOAD_FORM') {
return this.loadForm(receivedMessage.data?.contents ?? []) as undefined
}
}
private readonly saveForm = (): CommunicateMessage => {
const inputElements = this.findInputElements()
return {
type: 'FORM_SAVED',
data: {
url: window.location.href,
contents: [
...this.retrieveTextInputValues(inputElements)
]
}
}
}
private readonly loadForm = (formSaved: FormSavedDataContent[]): void => {
this.loadTextInputValues(formSaved)
}
// ---
private readonly findInputElements = (): HTMLInputElement[] =>
[...document.querySelectorAll('input')]
private readonly retrieveTextInputValues = (elements: HTMLInputElement[]): FormSavedDataContent[] =>
elements
.filter((v) => v.type === 'text')
.map((v) => ({
selector: `div#${v.parentElement?.id ?? ''}>input`,
type: 'TEXT' as const,
value: v.value
}))
// ---
private readonly loadTextInputValues = (formSaved: FormSavedDataContent[]): void =>
formSaved
.filter((v) => v.type === 'TEXT')
.forEach(this.loadTextInputValue.bind(this))
private readonly loadTextInputValue = (formSaved: FormSavedDataContent): void => {
const element = document.querySelector(formSaved.selector)
if (element === null || !(element instanceof HTMLInputElement) || element.type !== 'text') { return }
element.value = formSaved.value
element.dispatchEvent(new Event('input', {
bubbles: true
}))
}
}
new FormSaverExtension()
.startListen()

BIN
icons/koishi.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,14 +1,32 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "force_delete", "name": "Koishi ~~ Hello- I'm behind you right now. ~~",
"version": "1.0", "version": "1.0",
"description": "automatically types AWS's delete-safe words", "description": "Hello- I'm behind you right now.",
"icons": {
"48": "icons/koishi.webp"
},
"action": {
"default_icon": "icons/koishi.webp",
"default_title": "Koishi",
"default_popup": "popup/index.html"
},
"host_permissions": [
"*://*.console.aws.amazon.com/*"
],
"content_scripts": [ "content_scripts": [
{ {
"matches": ["*://*.console.aws.amazon.com/*"], "matches": ["*://*.console.aws.amazon.com/*"],
"js": ["main.js"], "js": [
"force_delete.js",
"form_saver.js"
],
"all_frames": true, "all_frames": true,
"match_about_blank": true, "match_about_blank": true,
"match_origin_as_fallback": true "match_origin_as_fallback": true

File diff suppressed because it is too large Load Diff

29
popup/index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/popup/main.css">
<title>Koishi Form Saver</title>
</head>
<body>
<h2>Form Saver</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>url</th>
<th>content</th>
<th>action</th>
</tr>
</thead>
<tbody id="render_point">
</tbody>
</table>
<button id="save">Save!!</button>
<script src="/popup/main.js"></script>
</body>
</html>

34
popup/main.css Normal file
View File

@ -0,0 +1,34 @@
:root {
background-color: #212121;
color: #fafafa;
font-family: Arial, Helvetica, sans-serif;
}
body {
padding: 10px;
}
* {
padding: 0;
margin: 0;
font-size: 14px;
}
table {
user-select: none;
}
button {
background-color: #626262;
padding: 2px 5px;
color: #fafafa;
border: none;
outline: 1px solid #af7d85;
border-radius: 6px;
cursor: pointer;
}
button:hover {
background-color: #838383;
}

83
popup/main.ts Normal file
View File

@ -0,0 +1,83 @@
import { CommunicateMessage, FormSavedData } from '../types'
void (async () => {
const api = browser ?? chrome
const [tab] = await api.tabs.query({
active: true,
currentWindow: true
})
if (tab.url === undefined) {
document.body.innerText = 'Extension disabled on this page'
return
}
if (localStorage.getItem('form_saved') === null) { localStorage.setItem('form_saved', '[]') }
document.getElementById('save')?.addEventListener('click', () => {
void (async () => {
const formSaved: CommunicateMessage = await api.tabs.sendMessage(tab.id ?? 0, {
type: 'SAVE_FORM'
})
if (formSaved.type !== 'FORM_SAVED') { return }
const prevSaved = JSON.parse(localStorage.getItem('form_saved') ?? '') as Array<FormSavedData & { id: string }>
const newSaved = JSON.stringify([
{
id: crypto.randomUUID(),
...formSaved.data
},
...prevSaved.slice(0, 10)
])
localStorage.setItem('form_saved', newSaved)
renderTable()
})()
})
renderTable()
function renderTable (): void {
const formSaved = JSON.parse(localStorage.getItem('form_saved') ?? '') as Array<FormSavedData & { id: string }>
const renderPoint = document.getElementById('render_point')
if (renderPoint === null) {
return
}
renderPoint.innerHTML = ''
for (const form of formSaved) {
renderPoint.innerHTML += `
<tr>
<td>${form.id.substring(0, 8)}</td>
<td>${new URL(form.url).pathname}${new URL(form.url).hash}</td>
<td>${form.contents.filter((v) => v.type === 'TEXT').length} Texts</td>
<td>
<button id="load-${form.id}">Load</button>
<button id="delete-${form.id}">x</button>
</td>
</tr>
`
}
for (const form of formSaved) {
document.getElementById(`load-${form.id}`)?.addEventListener('click', () => {
void api.tabs.sendMessage<CommunicateMessage>(tab.id ?? 0, {
type: 'LOAD_FORM',
data: form
})
})
document.getElementById(`delete-${form.id}`)?.addEventListener('click', () => {
const prevSaved = JSON.parse(localStorage.getItem('form_saved') ?? '') as Array<FormSavedData & { id: string }>
const newSaved = JSON.stringify(prevSaved.filter((v) => v.id !== form.id))
localStorage.setItem('form_saved', newSaved)
renderTable()
})
}
}
})()

View File

@ -25,7 +25,7 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "Preserve", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
@ -108,6 +108,6 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [ "include": [
"main.ts" "**/*.ts"
] ]
} }

22
types.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
import type { Browser } from 'webextension-polyfill'
declare global {
const browser: Browser | undefined
const chrome: Browser
}
interface CommunicateMessage {
type: 'SAVE_FORM' | 'LOAD_FORM' | 'FORM_SAVED',
data?: FormSavedData
}
interface FormSavedData {
url: string
contents: FormSavedDataContent[]
}
interface FormSavedDataContent {
selector: string,
type: 'TEXT',
value: string
}