Skip to content

Commit fd00dd4

Browse files
committed
feat(options): add custom payload input with variable autocompletion
1 parent e9db93d commit fd00dd4

6 files changed

Lines changed: 391 additions & 6 deletions

File tree

options/options.css

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,21 @@ label {
4545
}
4646

4747
input[type="text"],
48-
input[type="url"] {
48+
input[type="url"],
49+
textarea {
4950
width: 100%;
5051
padding: 10px;
5152
border: 1px solid #ddd;
5253
border-radius: 6px;
5354
box-sizing: border-box;
5455
}
5556

57+
textarea {
58+
min-height: 150px;
59+
font-family: monospace;
60+
resize: vertical;
61+
}
62+
5663
button {
5764
background-color: #007bff;
5865
color: white;
@@ -120,3 +127,53 @@ button:hover {
120127
background-color: #f8f9fa;
121128
border-radius: 6px;
122129
}
130+
131+
.textarea-container {
132+
position: relative;
133+
}
134+
135+
.autocomplete-container {
136+
position: absolute;
137+
background-color: white;
138+
border: 1px solid #ddd;
139+
border-radius: 4px;
140+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
141+
max-height: 200px;
142+
overflow-y: auto;
143+
width: 100%;
144+
z-index: 10;
145+
}
146+
147+
.autocomplete-item {
148+
padding: 8px 12px;
149+
cursor: pointer;
150+
}
151+
152+
.autocomplete-item:hover {
153+
background-color: #f0f2f5;
154+
}
155+
156+
.variables-help {
157+
margin-top: 10px;
158+
padding: 10px;
159+
background-color: #f8f9fa;
160+
border-radius: 6px;
161+
font-size: 0.9em;
162+
}
163+
164+
.variables-help p {
165+
margin-top: 0;
166+
font-weight: 600;
167+
}
168+
169+
.variables-help ul {
170+
margin: 0;
171+
padding-left: 20px;
172+
}
173+
174+
.variables-help code {
175+
background-color: #e9ecef;
176+
padding: 2px 4px;
177+
border-radius: 3px;
178+
font-family: monospace;
179+
}

options/options.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ <h2>__MSG_optionsAddWebhookHeader__</h2>
5151
<label for="webhook-identifier">Identifier (optional)</label>
5252
<input type="text" id="webhook-identifier" placeholder="Optional identifier" />
5353
</div>
54+
<div class="form-group">
55+
<label for="webhook-custom-payload">Custom Payload (optional)</label>
56+
<div class="textarea-container">
57+
<textarea id="webhook-custom-payload" placeholder="Enter custom JSON payload"></textarea>
58+
<div id="variables-autocomplete" class="autocomplete-container hidden"></div>
59+
</div>
60+
<div class="variables-help">
61+
<p>Available variables:</p>
62+
<ul>
63+
<li><code>{{tab.title}}</code> - Current tab title</li>
64+
<li><code>{{tab.url}}</code> - Current tab URL</li>
65+
<li><code>{{tab.id}}</code> - Current tab ID</li>
66+
<li><code>{{tab.windowId}}</code> - Current window ID</li>
67+
<li><code>{{tab.index}}</code> - Tab index</li>
68+
<li><code>{{tab.pinned}}</code> - Is tab pinned</li>
69+
<li><code>{{tab.audible}}</code> - Is tab playing audio</li>
70+
<li><code>{{tab.incognito}}</code> - Is tab in incognito mode</li>
71+
<li><code>{{tab.status}}</code> - Tab loading status</li>
72+
<li><code>{{browser}}</code> - Browser information</li>
73+
<li><code>{{platform}}</code> - Platform information</li>
74+
<li><code>{{triggeredAt}}</code> - Timestamp when triggered</li>
75+
<li><code>{{identifier}}</code> - Custom identifier</li>
76+
</ul>
77+
</div>
78+
</div>
5479
<button type="submit">__MSG_optionsSaveButton__</button>
5580
<button type="button" id="cancel-edit-btn" class="hidden">__MSG_optionsCancelEditButton__</button>
5681
</form>

options/options.js

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,115 @@ const headerKeyInput = document.getElementById("header-key");
7070
const headerValueInput = document.getElementById("header-value");
7171
const addHeaderBtn = document.getElementById("add-header-btn");
7272
const cancelEditBtn = document.getElementById("cancel-edit-btn");
73+
const customPayloadInput = document.getElementById("webhook-custom-payload");
74+
const variablesAutocomplete = document.getElementById("variables-autocomplete");
7375
let headers = [];
7476

77+
// Define available variables for autocompletion
78+
const availableVariables = [
79+
"{{tab.title}}", "{{tab.url}}", "{{tab.id}}", "{{tab.windowId}}",
80+
"{{tab.index}}", "{{tab.pinned}}", "{{tab.audible}}", "{{tab.incognito}}",
81+
"{{tab.status}}", "{{browser}}", "{{platform}}", "{{triggeredAt}}", "{{identifier}}"
82+
];
83+
84+
// Implement autocompletion for custom payload
85+
customPayloadInput.addEventListener('input', function(e) {
86+
const cursorPosition = this.selectionStart;
87+
const text = this.value;
88+
const textBeforeCursor = text.substring(0, cursorPosition);
89+
90+
// Check if we're typing a variable (starts with {{)
91+
const match = textBeforeCursor.match(/\{\{[\w\.\-]*$/);
92+
93+
if (match) {
94+
const partialVar = match[0];
95+
const matchingVars = availableVariables.filter(v => v.startsWith(partialVar));
96+
97+
if (matchingVars.length > 0) {
98+
// Show autocomplete dropdown
99+
variablesAutocomplete.innerHTML = '';
100+
variablesAutocomplete.classList.remove('hidden');
101+
102+
matchingVars.forEach(variable => {
103+
const item = document.createElement('div');
104+
item.className = 'autocomplete-item';
105+
item.textContent = variable;
106+
item.addEventListener('click', () => {
107+
// Insert the variable at cursor position
108+
const beforeVar = text.substring(0, cursorPosition - partialVar.length);
109+
const afterVar = text.substring(cursorPosition);
110+
const newText = beforeVar + variable + afterVar;
111+
customPayloadInput.value = newText;
112+
113+
// Move cursor after the inserted variable
114+
const newCursorPos = beforeVar.length + variable.length;
115+
customPayloadInput.focus();
116+
customPayloadInput.setSelectionRange(newCursorPos, newCursorPos);
117+
118+
// Hide autocomplete
119+
variablesAutocomplete.classList.add('hidden');
120+
});
121+
variablesAutocomplete.appendChild(item);
122+
});
123+
} else {
124+
variablesAutocomplete.classList.add('hidden');
125+
}
126+
} else {
127+
variablesAutocomplete.classList.add('hidden');
128+
}
129+
});
130+
131+
// Hide autocomplete when clicking outside
132+
document.addEventListener('click', function(e) {
133+
if (!customPayloadInput.contains(e.target) && !variablesAutocomplete.contains(e.target)) {
134+
variablesAutocomplete.classList.add('hidden');
135+
}
136+
});
137+
138+
// Add keyboard navigation for autocomplete
139+
customPayloadInput.addEventListener('keydown', function(e) {
140+
if (variablesAutocomplete.classList.contains('hidden')) return;
141+
142+
const items = variablesAutocomplete.querySelectorAll('.autocomplete-item');
143+
const activeItem = variablesAutocomplete.querySelector('.autocomplete-item.active');
144+
let activeIndex = -1;
145+
146+
if (activeItem) {
147+
activeIndex = Array.from(items).indexOf(activeItem);
148+
}
149+
150+
switch (e.key) {
151+
case 'ArrowDown':
152+
e.preventDefault();
153+
if (activeIndex < items.length - 1) {
154+
if (activeItem) activeItem.classList.remove('active');
155+
items[activeIndex + 1].classList.add('active');
156+
items[activeIndex + 1].scrollIntoView({ block: 'nearest' });
157+
}
158+
break;
159+
case 'ArrowUp':
160+
e.preventDefault();
161+
if (activeIndex > 0) {
162+
if (activeItem) activeItem.classList.remove('active');
163+
items[activeIndex - 1].classList.add('active');
164+
items[activeIndex - 1].scrollIntoView({ block: 'nearest' });
165+
}
166+
break;
167+
case 'Enter':
168+
e.preventDefault();
169+
if (activeItem) {
170+
activeItem.click();
171+
} else if (items.length > 0) {
172+
items[0].click();
173+
}
174+
break;
175+
case 'Escape':
176+
e.preventDefault();
177+
variablesAutocomplete.classList.add('hidden');
178+
break;
179+
}
180+
});
181+
75182
function renderHeaders() {
76183
headersListDiv.textContent = '';
77184
headers.forEach((header, idx) => {
@@ -128,12 +235,21 @@ form.addEventListener("submit", async (e) => {
128235
const url = urlInput.value.trim();
129236
const method = methodSelect.value;
130237
const identifier = identifierInput.value.trim();
238+
const customPayload = customPayloadInput.value.trim();
131239
let { webhooks = [] } = await browser.storage.sync.get("webhooks");
132240

133241
if (editWebhookId) {
134242
// Edit mode: update existing webhook
135243
webhooks = webhooks.map((wh) =>
136-
wh.id === editWebhookId ? { ...wh, label, url, method, headers: [...headers], identifier } : wh
244+
wh.id === editWebhookId ? {
245+
...wh,
246+
label,
247+
url,
248+
method,
249+
headers: [...headers],
250+
identifier,
251+
customPayload: customPayload || null
252+
} : wh
137253
);
138254
editWebhookId = null;
139255
cancelEditBtn.classList.add("hidden");
@@ -146,6 +262,7 @@ form.addEventListener("submit", async (e) => {
146262
method,
147263
headers: [...headers],
148264
identifier,
265+
customPayload: customPayload || null
149266
};
150267
webhooks.push(newWebhook);
151268
}
@@ -155,6 +272,9 @@ form.addEventListener("submit", async (e) => {
155272
urlInput.value = "";
156273
methodSelect.value = "POST";
157274
identifierInput.value = "";
275+
customPayloadInput.value = "";
276+
headerKeyInput.value = "";
277+
headerValueInput.value = "";
158278
headers = [];
159279
renderHeaders();
160280
// Always reset to save button after submit
@@ -182,6 +302,8 @@ webhookList.addEventListener("click", async (e) => {
182302
urlInput.value = "";
183303
methodSelect.value = "POST";
184304
identifierInput.value = "";
305+
headerKeyInput.value = "";
306+
headerValueInput.value = "";
185307
headers = [];
186308
renderHeaders();
187309
cancelEditBtn.classList.add("hidden");
@@ -196,6 +318,7 @@ webhookList.addEventListener("click", async (e) => {
196318
urlInput.value = webhook.url;
197319
methodSelect.value = webhook.method || "POST";
198320
identifierInput.value = webhook.identifier || "";
321+
customPayloadInput.value = webhook.customPayload || "";
199322
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
200323
renderHeaders();
201324
cancelEditBtn.classList.remove("hidden");
@@ -213,6 +336,9 @@ cancelEditBtn.addEventListener("click", () => {
213336
urlInput.value = "";
214337
methodSelect.value = "POST";
215338
identifierInput.value = "";
339+
customPayloadInput.value = "";
340+
headerKeyInput.value = "";
341+
headerValueInput.value = "";
216342
headers = [];
217343
renderHeaders();
218344
cancelEditBtn.classList.add("hidden");

popup/popup.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ document
7676
const browserInfo = await browser.runtime.getBrowserInfo?.() || {};
7777
const platformInfo = await browser.runtime.getPlatformInfo?.() || {};
7878

79-
const payload = {
79+
// Create default payload
80+
let payload = {
8081
tab: {
8182
title: activeTab.title,
8283
url: currentUrl,
@@ -93,9 +94,62 @@ document
9394
platform: platformInfo,
9495
triggeredAt: new Date().toISOString(),
9596
};
97+
9698
if (webhook && webhook.identifier) {
9799
payload.identifier = webhook.identifier;
98100
}
101+
102+
// Use custom payload if available
103+
// The custom payload is a JSON string that can contain placeholders like {{tab.title}}
104+
// These placeholders will be replaced with actual values before sending the webhook
105+
if (webhook && webhook.customPayload) {
106+
try {
107+
// Create variable replacements map
108+
const replacements = {
109+
"{{tab.title}}": activeTab.title,
110+
"{{tab.url}}": currentUrl,
111+
"{{tab.id}}": activeTab.id,
112+
"{{tab.windowId}}": activeTab.windowId,
113+
"{{tab.index}}": activeTab.index,
114+
"{{tab.pinned}}": activeTab.pinned,
115+
"{{tab.audible}}": activeTab.audible,
116+
"{{tab.incognito}}": activeTab.incognito,
117+
"{{tab.status}}": activeTab.status,
118+
"{{browser}}": JSON.stringify(browserInfo),
119+
"{{platform}}": JSON.stringify(platformInfo),
120+
"{{triggeredAt}}": new Date().toISOString(),
121+
"{{identifier}}": webhook.identifier || ""
122+
};
123+
124+
// Replace placeholders in custom payload
125+
let customPayloadStr = webhook.customPayload;
126+
Object.entries(replacements).forEach(([placeholder, value]) => {
127+
// Handle different types of values
128+
// For string values in JSON, we need to handle them differently based on context
129+
// If the placeholder is inside quotes in the JSON, we should not add quotes again
130+
const isPlaceholderInQuotes = customPayloadStr.match(new RegExp(`"[^"]*${placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^"]*"`, 'g'));
131+
132+
const replaceValue = typeof value === 'string'
133+
? (isPlaceholderInQuotes ? value.replace(/"/g, '\\"') : `"${value.replace(/"/g, '\\"')}"`)
134+
: (value === undefined ? 'null' : JSON.stringify(value));
135+
136+
customPayloadStr = customPayloadStr.replace(
137+
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
138+
replaceValue
139+
);
140+
});
141+
142+
// Parse the resulting JSON
143+
const customPayload = JSON.parse(customPayloadStr);
144+
console.log(customPayload);
145+
146+
// Use the custom payload instead of the default one
147+
payload = customPayload;
148+
} catch (error) {
149+
console.error("Error parsing custom payload:", error);
150+
// Fall back to default payload if custom payload is invalid
151+
}
152+
}
99153
// Prepare headers
100154
let headers = { "Content-Type": "application/json" };
101155
if (webhook && Array.isArray(webhook.headers)) {

0 commit comments

Comments
 (0)