Skip to content

Commit e10e8d2

Browse files
committed
Addressing feedback from PR GoogleChrome#612
1 parent e620662 commit e10e8d2

5 files changed

Lines changed: 100 additions & 92 deletions

File tree

functional-samples/sample.text-replacer/background.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,47 @@
1-
chrome.commands.onCommand.addListener((command, tab) => {
2-
if (command == 'replace-text') {
3-
replaceText(tab.id);
4-
}
5-
});
1+
const REPLACE_TEXT_MENUITEM_ID = 'replace-text-menuitem';
2+
const REPLACE_TEXT_COMMAND_ID = 'replace-text-command';
3+
const GET_REPLACEMENTS_MESSAGE_ID = 'get-replacements';
4+
const SET_REPLACEMENTS_MESSAGE_ID = 'set-replacements';
65

76
chrome.runtime.onInstalled.addListener(async () => {
87
// Remove any previously registered context menus to avoid conflicts
98
await chrome.contextMenus.removeAll();
109

1110
// Register the new set of context menus
1211
await chrome.contextMenus.create({
13-
id: 'replace-text-menuitem',
12+
id: REPLACE_TEXT_MENUITEM_ID,
1413
title: 'Replace text',
1514
});
1615
});
1716

17+
chrome.commands.onCommand.addListener((command, tab) => {
18+
if (command == REPLACE_TEXT_COMMAND_ID) {
19+
replaceText(tab.id);
20+
} else {
21+
throw new Error(`Unknown command executed with ID "${command}"`);
22+
}
23+
});
24+
1825
chrome.contextMenus.onClicked.addListener((info, tab) => {
19-
if (info.menuItemId == 'replace-text-menuitem') {
26+
if (info.menuItemId == REPLACE_TEXT_MENUITEM_ID) {
2027
replaceText(tab.id);
28+
} else {
29+
throw new Error(`Unknown context menu option clicked with ID "${info.menuItemId}"`);
30+
}
31+
});
32+
33+
// Route all storage reads/writes through the background so we have a single
34+
// source of truth.
35+
chrome.runtime.onMessage.addListener((message, ) => {
36+
switch (message.id) {
37+
case GET_REPLACEMENTS_MESSAGE_ID:
38+
return chrome.storage.sync.get(['patterns']);
39+
40+
case SET_REPLACEMENTS_MESSAGE_ID:
41+
return chrome.storage.sync.set({'patterns': message.data});
42+
43+
default:
44+
throw new Error(`Unknown message received with ID "${message.id}"`);
2145
}
2246
});
2347

functional-samples/sample.text-replacer/content.js

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,50 @@
44
// license that can be found in the LICENSE file or at
55
// https://developers.google.com/open-source/licenses/bsd
66

7-
// Replace text on the page using a static list of patterns
8-
function textReplacer(replacements) {
9-
const replacementPatterns = buildReplacementRegex(replacements);
10-
replaceText(replacementPatterns);
11-
}
12-
13-
function buildReplacementRegex(source) {
14-
const output = [];
15-
for (var i = 0; i < source.length; i++) {
16-
if (!source[i]) { continue; }
17-
const [find, replace] = source[i];
18-
const sanitizedMatch = escapeRegExp(find);
19-
const findExp = new RegExp(`\\b${sanitizedMatch}\\b`, 'gi');
20-
output[i] = [findExp, replace];
7+
// Wrap the in an immediately invoked function expression (IIFE) in order to
8+
// prevent local variables from polluting global scope
9+
(function () {
10+
const GET_REPLACEMENTS_MESSAGE_ID = 'get-replacements';
11+
12+
13+
function buildReplacementRegex(source) {
14+
const output = [];
15+
for (let i = 0; i < source.length; i++) {
16+
if (!source[i]) { continue; }
17+
const [find, replace] = source[i];
18+
const sanitizedMatch = escapeRegExp(find);
19+
const findExp = new RegExp(`\\b${sanitizedMatch}\\b`, 'gi');
20+
output[i] = [findExp, replace];
21+
}
22+
return output;
23+
}
24+
25+
// This may not cover all special characters used in regular repressions. For
26+
// example purposes only.
27+
var REGEXP_SPECIAL_CHARACTERS = /[.(){}^$*+?[\]\\]/g;
28+
/** Sanitize user input to prevent unexpected behavior during RegExp execution */
29+
function escapeRegExp(pattern) {
30+
return pattern.replace(REGEXP_SPECIAL_CHARACTERS, "\\$&")
2131
}
22-
return output;
23-
}
24-
25-
// Use var to avoid "Identifier 'REGEXP_SPECIAL_CHARACTERS' has already been
26-
// declared" errors when running multiple times on the same page.
27-
var REGEXP_SPECIAL_CHARACTERS = /[.(){}^$*+?[\]\\]/g;
28-
/** Sanitize user input to prevent unexpected behavior during RegExp execution */
29-
function escapeRegExp(pattern) {
30-
return pattern.replace(REGEXP_SPECIAL_CHARACTERS, "\\$&")
31-
}
32-
33-
/** Iterate through all text nodes and replace */
34-
function replaceText(replacements) {
35-
let node;
36-
const nodeIterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT);
37-
while (node = nodeIterator.nextNode()) {
38-
for (let [find, replace] of replacements) {
39-
node.nodeValue = node.nodeValue.replace(find, replace);
32+
33+
/** Iterate through all text nodes and replace */
34+
function replaceText(replacements) {
35+
const nodeIterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT);
36+
37+
let node;
38+
while (node = nodeIterator.nextNode()) {
39+
for (let [find, replace] of replacements) {
40+
node.nodeValue = node.nodeValue.replace(find, replace);
41+
}
4042
}
4143
}
42-
}
4344

44-
// Replace text on the page using a list of patterns loaded from storage
45-
chrome.storage.sync.get(['patterns'], function(data) {
46-
textReplacer(data.patterns);
47-
});
45+
// Get the patterns to replace from storage, then build a regex from them and
46+
// replace all text on the page.
47+
chrome.runtime.sendMessage({ id: GET_REPLACEMENTS_MESSAGE_ID })
48+
.then(function(data) {
49+
const replacementPatterns = buildReplacementRegex(data.patterns);
50+
replaceText(replacementPatterns);
51+
});
52+
53+
})()

functional-samples/sample.text-replacer/manifest.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"64": "icons/icon64.png"
1010
},
1111
"action": {
12-
"default_title": "Show an alert",
12+
"default_title": "Manage text replacements",
1313
"default_icon": {
1414
"16": "icons/icon16.png",
1515
"24": "icons/icon24.png",
@@ -28,8 +28,12 @@
2828
"service_worker": "background.js"
2929
},
3030
"commands": {
31-
"replace-text": {
32-
"description": "Replace text on the current page."
31+
"replace-text-command": {
32+
"description": "Replace text on the current page.",
33+
"suggested_key": {
34+
"default": "Alt+R",
35+
"mac": "MacCtrl+R"
36+
}
3337
}
3438
}
3539
}

functional-samples/sample.text-replacer/popup.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@
6969
</table>
7070

7171
<div class="form--controls">
72-
<input id="clear" type="button" value="Clear">
73-
<input id="submit" type="submit" value="Replace">
72+
<input id="reset" type="reset" value="Clear" />
73+
<input id="submit" type="submit" value="Replace" />
7474
</div>
7575
</form>
7676

functional-samples/sample.text-replacer/popup.js

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
// license that can be found in the LICENSE file or at
55
// https://developers.google.com/open-source/licenses/bsd
66

7+
const GET_REPLACEMENTS_MESSAGE_ID = 'get-replacements';
8+
const SET_REPLACEMENTS_MESSAGE_ID = 'set-replacements';
79
const form = document.querySelector('form');
810

9-
load(['patterns'])
10-
.then(data => data.patterns)
11-
.then(loadFormData);
11+
// Ask the background for replacement patterns and initialize the page
12+
chrome.runtime.sendMessage({id: GET_REPLACEMENTS_MESSAGE_ID})
13+
.then(data => loadFormData(data.patterns));
1214

1315
form.addEventListener('submit', async (event) => {
1416
event.preventDefault();
@@ -21,9 +23,12 @@ form.addEventListener('submit', async (event) => {
2123
});
2224
});
2325

24-
document.getElementById('clear').addEventListener('click', (event) => {
25-
form.querySelectorAll('input[type=text]').forEach(el => el.value = '');
26-
saveFormData();
26+
document.getElementById('reset').addEventListener('click', (event) => {
27+
// <input type="reset"> automatically reset the form's contents, but those
28+
// changes won't settle until the the next turn of the event loop. Since
29+
// saveFormData works directly gainst DOM, we have to call it after a minimal
30+
// delay.
31+
setTimeout(saveFormData, 0);
2732
});
2833

2934
form.addEventListener('input', debounce(saveFormData, 250));
@@ -47,7 +52,7 @@ function saveFormData() {
4752
const inputs = form.querySelectorAll('input[type=text]');
4853

4954
const patterns = [...inputs].reduce((acc, input, index) => {
50-
const outerIndex = index >> 1;
55+
const outerIndex = Math.floor(index / 2);
5156
const innerIndex = index % 2;
5257

5358
if (innerIndex === 0) {
@@ -61,7 +66,11 @@ function saveFormData() {
6166
return acc;
6267
}, []);
6368

64-
return save({patterns});
69+
// Ask the background to persist this data
70+
chrome.runtime.sendMessage({
71+
id: SET_REPLACEMENTS_MESSAGE_ID,
72+
data: patterns,
73+
});
6574
}
6675

6776
/**
@@ -93,38 +102,3 @@ async function getCurrentTab() {
93102
let [tab] = await chrome.tabs.query(queryOptions);
94103
return tab;
95104
}
96-
97-
98-
/**
99-
* Minimal promise wrapper for chrome.storage.sync.set().
100-
*
101-
* @param {object} data Object containing key-value pairs of data to persist.
102-
*/
103-
async function save(data) {
104-
return new Promise((resolve, reject) => {
105-
chrome.storage.sync.set(data, (result) => {
106-
if (chrome.runtime.lastError) {
107-
reject(chrome.runtime.lastError);
108-
} else {
109-
resolve(result);
110-
}
111-
});
112-
});
113-
}
114-
115-
/**
116-
* Minimal promise wrapper for chrome.storage.sync.get().
117-
*
118-
* @param {string[]} keys Array of keys to retrieve from storage.
119-
*/
120-
function load(keys) {
121-
return new Promise((resolve, reject) => {
122-
chrome.storage.sync.get(keys, (data) => {
123-
if (chrome.runtime.lastError) {
124-
reject(chrome.runtime.lastError);
125-
} else {
126-
resolve(data);
127-
}
128-
});
129-
});
130-
}

0 commit comments

Comments
 (0)