Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New sample] Open Chrome API reference page (omnibox, alarms, messaging sample) #848

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
81 changes: 81 additions & 0 deletions functional-samples/tutorial.open-api-reference/api-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export default [
{
content: 'commands',
description:
'Use the <match>Commands API</match> to add a keyboard shortcuts.'
},
{
content: 'contextmenus',
description:
"Use the <match>ContextMenus API</match> to add a custom item to Chrome's context menu."
},
{
content: 'declarativeNetRequest',
description:
'Use the <match>DeclarativeNetRequest API</match> to block or modify network requests.'
},
{
content: 'downloads',
description:
'Use the <match>Downloads API</match> to programmatically manipulate downloads.'
},
{
content: 'i18n',
description: 'Use the <match>i18n API</match> to localize your extension'
},
{
content: 'identity',
description:
'Use the <match>Identity API</match> to get OAuth2 access tokens.'
},
{
content: 'notifications',
description:
'Use the <match>Notifications API</match> show notifications to users in the system tray.'
},
{
content: 'offscreen',
description:
'Use the <match>Offscreen API</match> to create and manage offscreen documents.'
},
{
content: 'omnibox',
description:
"Use the <match>Omnibox API</match> to register a keyword with Chrome's address bar."
},
{
content: 'permissions',
description:
'Use the <match>Permissions API</match> to request optional permissions at run time.'
},
{
content: 'runtime',
description:
'Use the <match>Runtime API</match> pass messages, manage extension lifecycle, and access other helper utils.'
},
{
content: 'scripting',
description:
'Use the <match>Scripting API</match> to execute scripts in different contexts.'
},
{
content: 'storage',
description:
'Use the <match>Storage API</match> to store, retrieve, and track changes to user data.'
},
{
content: 'tabs',
description:
'Use the <match>Tabs API</match> to create, update and manipulate tabs.'
},
{
content: 'topSites',
description:
'Use the <match>TopSites API</match> to access the most visited sites that are displayed on the new tab page.'
},
{
content: 'webNavigation',
description:
'Use the <match>WebNavigation API</match> to receive notifications about the status of navigation requests in-flight.'
}
];
30 changes: 30 additions & 0 deletions functional-samples/tutorial.open-api-reference/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Popover API https://chromestatus.com/feature/5463833265045504

(async () => {
const nav = document.querySelector('.navigation-rail__links');

const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });

const tipWidget = createDomElement(`
<button class="navigation-rail__link" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0; border: none; background: none;>
<div class="navigation-rail__icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d='M15 16H9M14.5 9C14.5 7.61929 13.3807 6.5 12 6.5M6 9C6 11.2208 7.2066 13.1599 9 14.1973V18.5C9 19.8807 10.1193 21 11.5 21H12.5C13.8807 21 15 19.8807 15 18.5V14.1973C16.7934 13.1599 18 11.2208 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9Z'"></path>
</svg>
</div>
<span>Tip</span>
</button>
`);

const popover = createDomElement(
`<div id='tip-popover' popover>${tip}</div>`
);

document.body.append(popover);
nav.append(tipWidget);
})();

function createDomElement(html) {
const dom = new DOMParser().parseFromString(html, 'text/html');
return dom.body.firstElementChild;
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions functional-samples/tutorial.open-api-reference/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"manifest_version": 3,
"name": "Open extension API reference",
"version": "1.0.0",
"icons": {
"16": "icon-16.png",
"128": "icon-128.png"
},
"background": {
"service_worker": "service-worker.js",
"type": "module"
},
"minimum_chrome_version": "102",
"omnibox": {
"keyword": "api"
},
"permissions": ["alarms", "storage"],
"content_scripts": [
{
"matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
"js": ["content.js"]
}
],
"host_permissions": ["https://extension-tips.glitch.me/*"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './sw-omnibox.js';
import './sw-tips.js';
36 changes: 36 additions & 0 deletions functional-samples/tutorial.open-api-reference/sw-omnibox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getApiSuggestions } from './sw-suggestions.js';

console.log('sw-omnibox.js');
sebastianbenz marked this conversation as resolved.
Show resolved Hide resolved

// Save default API suggestions
AmySteam marked this conversation as resolved.
Show resolved Hide resolved
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.storage.local.set({
apiSuggestions: ['tabs', 'storage', 'scripting']
});
}
});

const chromeURL = 'https://developer.chrome.com/docs/extensions/reference/';
AmySteam marked this conversation as resolved.
Show resolved Hide resolved
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Displays the suggestions after user starts typing
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
const { description, suggestions } = await getApiSuggestions(input);
await chrome.omnibox.setDefaultSuggestion({ description });
suggest(suggestions);
});

// Opens the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
chrome.tabs.create({ url: chromeURL + input });
// Saves the latest keyword
updateHistory(input);
});

async function updateHistory(input) {
const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
apiSuggestions.unshift(input);
apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
await chrome.storage.local.set({ apiSuggestions });
}
24 changes: 24 additions & 0 deletions functional-samples/tutorial.open-api-reference/sw-suggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import apiList from './api-list.js';

/**
* Returns a list of suggestions and a description for the default suggestion
*/
export async function getApiSuggestions(input) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you pick a name that better explains what input is?

const filtered = apiList.filter((api) => api.content.startsWith(input));
AmySteam marked this conversation as resolved.
Show resolved Hide resolved
console.log('filtered', filtered);
AmySteam marked this conversation as resolved.
Show resolved Hide resolved

// return suggestions if any exist
if (filtered.length) {
return {
description: 'Matching Chrome APIs',
suggestions: filtered
};
}

// return past searches if no match was found
const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
return {
description: 'No matches found. Choose from past searches',
suggestions: apiList.filter((item) => apiSuggestions.includes(item.content))
};
}
28 changes: 28 additions & 0 deletions functional-samples/tutorial.open-api-reference/sw-tips.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
console.log('sw-tips.js');
sebastianbenz marked this conversation as resolved.
Show resolved Hide resolved

// Fetch tip & save in storage
const updateTip = async () => {
const response = await fetch('https://extension-tips.glitch.me/tips.json');
const tips = await response.json();
const randomIndex = Math.floor(Math.random() * tips.length);
await chrome.storage.local.set({ tip: tips[randomIndex] });
};

// Create a daily alarm and retrieves the first tip when extension is installed.
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.alarms.create({ delayInMinutes: 1, periodInMinutes: 1440 });
updateTip();
}
});

// Retrieve tip of the day
chrome.alarms.onAlarm.addListener(updateTip);

// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.greeting === 'tip') {
chrome.storage.local.get('tip').then(sendResponse);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use async await

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used .then() here b/c the return value of the onMessage listener determines if Chrome expects sendResponse to be called.

If we use async/await, this function will return a Promise, and Chrome will always expect sendResponse to be called.

We could wrap the call to .get() in an async IIFE, but it seems like overkill for this tutorial😊

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that, have you got a link to where this is explained. Curious to learn how this works.

Copy link
Contributor Author

@AmySteam AmySteam Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the section that explain this, but I think it can be explained better:

// Don't use an async function to handle messages 
// b/c an async function always returns a Promise object
// a Promise object is always truthy 
// regardless of the value that the Promise contains
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.greeting === "hello") {
        // start a call to get a value from storage
        chrome.storage.local.get("name").then(({ name }) => {
            // this runs after the onMessage handler returns
            sendResponse(`my name is ${name}`)
        })
        // the onMessage handler should return truthy if sendResponse will be called
        return true
    }
    // the onMessage handler should return falsy if sendResponse will not be called
    return false
})

returns boolean | undefined

Screenshot 2023-03-21 at 14 58 50

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ! That's not intuitive.

One thing: maybe rename the message field from greeting to type as it better expresses what this field is used for.

return true;
}
});