Skip to content

Commit

Permalink
Add GM.registerMenuCommand()
Browse files Browse the repository at this point in the history
Resolves one part of greasemonkey#2714 .
  • Loading branch information
esperecyan committed Dec 15, 2017
1 parent c36bbbe commit c5123f7
Show file tree
Hide file tree
Showing 11 changed files with 562 additions and 6 deletions.
54 changes: 54 additions & 0 deletions doc/Messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ Received by: `bg/is-enabled.js`.

Send this to toggle the global enabled status. No data.

# ListMenuCommands
Sent by: `browser/monkey-menu.js`
Received by: `bg/on-user-script-menu-command.js`

Triggered when the popup menu is loaded.
Lists menu commands registered by the `GM.registerMenuCommand()` method called by user scripts on the specified tab.

Data:

* `tabId` A tab's ID (integer).

Response data:

* An array of command objects. Each object has
- `id` A command id that can be used as HTML/XML ID.
- `caption` A string given as the first parameter of the `GM.registerMenuCommand()` method.
- `accessKey` A code point (string) or empty string given as the third parameter of the `GM.registerMenuCommand()` method.

# ListUserScripts
Received by: `bg/user-script-registry.js`.

Expand All @@ -125,6 +143,22 @@ progress as a percentage. Sent specifically back to the content process
* `errors` A (possibly empty) list of string error messages.
* `progress` A number, 0.0 to 1.0, representing the completion so far.

# MenuCommandClick
Sent by: `browser/monkey-menu.js`
Received by: `bg/on-user-script-menu-command.js`

Triggered when a command button on the popup menu is clicked by the user.
Posts message `{type: 'onclick'}` on `UserScriptMenuCommand` channel
to call a function given as the second parameter of the `GM.registerMenuCommand()` method.

Data:

* `id` The command id (string) as returned by `ListMenuCommands` message.

Response data:

* `undefined`

# UserScriptChanged
Sent by: `bg/user-script-registry.js`

Expand Down Expand Up @@ -158,6 +192,26 @@ user. Data:

Callers should specify one or the other, not both.

# UserScriptMenuCommand
Sent by: `content/api-provider-source.js`
Received by: `bg/on-user-script-menu-command.js`

This is a channel (not a message).
Triggered when the `GM.registerMenuCommand()` method is called by an user script.

Data:

* `details` The details object specifying the command. It has
- `caption` A string given as the first parameter of the `GM.registerMenuCommand()` method.
- `accessKey` A code point (string) or empty string given as the third parameter of the `GM.registerMenuCommand()` method.

Response data:

* `undefined`

Messages exchanged via this channel are private to its implementation.
See sender and receiver for further detail.

# UserScriptToggleEnabled
Sent by: `content/manage-user-scripts.js`
Received by: `bg/user-script-registry.js`
Expand Down
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"src/bg/on-message.js",
"src/bg/on-user-script-notification.js",
"src/bg/on-user-script-open-in-tab.js",
"src/bg/on-user-script-menu-command.js",
"src/bg/on-user-script-xhr.js",
"src/bg/user-script-install.js",
"src/bg/user-script-registry.js",
Expand Down
35 changes: 34 additions & 1 deletion src/bg/api-provider-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const SUPPORTED_APIS = new Set([
'GM.getResourceUrl',
'GM.notification',
'GM.openInTab',
'GM.registerMenuCommand',
'GM.setClipboard',
'GM.xmlHttpRequest',
]);
Expand Down Expand Up @@ -55,6 +56,10 @@ function apiProviderSource(userScript) {
source += 'GM.openInTab = ' + GM_openInTab.toString() + ';\n\n';
}

if (grants.includes('GM.registerMenuCommand')) {
source += 'GM.registerMenuCommand = ' + GM_registerMenuCommand.toString() + ';\n\n';
}

if (grants.includes('GM.setClipboard')) {
source += 'GM.setClipboard = ' + GM_setClipboard.toString() + ';\n\n';
}
Expand All @@ -63,7 +68,6 @@ function apiProviderSource(userScript) {
source += 'GM.xmlHttpRequest = ' + GM_xmlHttpRequest.toString() + ';\n\n';
}

// TODO: GM_registerMenuCommand -- maybe.
// TODO: GM_getResourceText -- maybe.

source += '})();';
Expand Down Expand Up @@ -196,6 +200,35 @@ function GM_openInTab(url, openInBackground) {
}


function GM_registerMenuCommand(caption, commandFunc, accessKey = '') {
if (typeof caption != 'string') {
caption = String(caption);
}

if (typeof commandFunc != 'function') {
throw new Error('GM.registerMenuCommand: "commandFunc" must be a function');
}

if (typeof accessKey != 'string' || Array.from(accessKey).length > 1) {
throw new Error('GM.registerMenuCommand: "accessKey" must be a single character');
}

let port = chrome.runtime.connect({name: 'UserScriptMenuCommand'});
port.onMessage.addListener(msg => {
if (msg.type === 'onclick') {
commandFunc();
}
});
port.postMessage({
name: 'register',
details: {
caption,
accessKey,
}
});
}


function GM_setClipboard(text) {
function onCopy(event) {
document.removeEventListener('copy', onCopy, true);
Expand Down
71 changes: 71 additions & 0 deletions src/bg/on-user-script-menu-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
This file is responsible for providing the GM.registerMenuCommand API method.
*/

// Private implementation
(function () {

const commandMap = new Map();


function _randomId() {
return 'id' + window.crypto.getRandomValues(new Uint8Array(16)).join('');
}


function onListMenuCommands(message, sender, sendResponse) {
sendResponse(
Array.from(commandMap.values())
.filter(command => command.port.sender.tab.id === message.tabId)
.map(command => ({
id: command.id,
caption: command.caption,
accessKey: command.accessKey,
}))
);
}
window.onListMenuCommands = onListMenuCommands;


function onMenuCommandClick(message, sender, sendResponse) {
if (commandMap.has(message.id)) {
commandMap.get(message.id).port.postMessage({type: 'onclick'});
}
}
window.onMenuCommandClick = onMenuCommandClick;


function registerMenuCommand({port, caption, accessKey}) {
const command = {
id: _randomId(),
port,
caption,
accessKey,
};

commandMap.set(command.id, command);

port.onDisconnect.addListener(port => {
commandMap.delete(command.id);
});
}


function onUserScriptMenuCommand(port) {
if (port.name != 'UserScriptMenuCommand') return;

port.onMessage.addListener((msg, port) => {
switch (msg.name) {
case 'register':
registerMenuCommand(Object.assign({
port,
}, msg.details));
break;
default:
console.warn('UserScriptMenuCommand port un-handled message name:', msg.name);
}
});
}
chrome.runtime.onConnect.addListener(onUserScriptMenuCommand);

})();
14 changes: 9 additions & 5 deletions src/browser/monkey-menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ body {
}
body.rendering { display: none; }

body #user-script-detail, body.detail #menu { display: none; }
body #menu, body.detail #user-script-detail { display: block; }
body #user-script-detail, body.detail #menu, body #menu-commands, body.commands #menu { display: none; }
body #menu, body.detail #user-script-detail, body.commands #menu-commands { display: block; }
#templates { display: none; }


Expand Down Expand Up @@ -43,7 +43,11 @@ body #menu, body.detail #user-script-detail { display: block; }
white-space: nowrap;
width: 0;
}
.menu-item:hover {
.menu-item .access-key {
text-decoration: underline;
text-transform: uppercase;
}
.menu-item:hover, .menu-item:focus {
background: #EEE;
}

Expand All @@ -61,10 +65,10 @@ header * {
padding: 6px;
vertical-align: middle;
}
header #back {
header #back, #menu-commands-back {
padding: 8px 6px 4px 6px;
}
header #back:hover {
header #back:hover, header #menu-commands-back:hover {
background: #EEE;
}
header #name {
Expand Down
23 changes: 23 additions & 0 deletions src/browser/monkey-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
</div>
-->

<div class="menu-item" id="open-menu-commands" rv-hide="menuCommands | empty">
<hr>
<div class="icon fa fa-fw fa-list-alt "></div>
<div class="text">User script commands ...</div>
</div>

<div rv-hide="userScripts.active | empty">
<hr>
<div class="menu-item">
Expand Down Expand Up @@ -76,6 +82,23 @@
</section>


<section id="menu-commands">
<header>
<div class="fa fa-lg fa-chevron-left" id="menu-commands-back"></div>
<div class="text heading">User script commands</div>
</header>
<hr>
<div rv-each-command="menuCommands" class="menu-item" tabindex="0"
rv-aria-keyshortcuts="command.accessKey"
rv-data-menu-command-id="command.id"
>
<div class="text">
{command.caption}<span rv-if="command.accessKey">(<span class="access-key">{command.accessKey}</span>)</span>
</div>
</div>
</section>


<section id="user-script-detail">
<header>
<div class="fa fa-lg fa-chevron-left" id="back"></div>
Expand Down
41 changes: 41 additions & 0 deletions src/browser/monkey-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ let gTplData = {
'active': [],
'inactive': [],
},
'menuCommands': [],
'pendingUninstall': 0,
};
let gUserScripts = {};
Expand Down Expand Up @@ -55,6 +56,8 @@ function onClick(event) {
if (el.id == 'back') {
goToTop();
return;
} else if (el.id == 'menu-commands-back') {
document.body.classList.remove('commands');
}

while (el && el.classList && !el.classList.contains('menu-item')) {
Expand All @@ -81,13 +84,21 @@ function onClick(event) {

gActiveUuid = uuid;
document.body.className = 'detail';
} else if (el.hasAttribute('data-menu-command-id')) {
chrome.runtime.sendMessage(
{'name': 'MenuCommandClick', 'id': el.getAttribute('data-menu-command-id')},
() => window.close());
} else switch (el.getAttribute('id')) {
case 'toggle-global-enabled':
chrome.runtime.sendMessage(
{'name': 'EnabledToggle'},
enabled => gTplData.enabled = enabled);
break;

case 'open-menu-commands':
document.body.classList.add('commands');
break;

case 'new-user-script':
let r = Math.floor(Math.random() * 900000 + 100000);
let newScriptSrc = gNewScriptTpl.replace('%d', r);
Expand Down Expand Up @@ -124,6 +135,27 @@ function onClick(event) {
}
}

function onKeyPress(event) {
if (document.body.classList.contains('commands')) {
if (Array.from(event.key).length === 1) {
const commands = document.querySelectorAll(
`#menu-commands [aria-keyshortcuts="${CSS.escape(event.key.toLowerCase())}" i]`
);

if (commands.length === 1) {
commands[0].click();
} else if (commands.length > 1) {
const nextIndex = Array.from(commands).findIndex(command => command.matches(':focus')) + 1;
commands[nextIndex < commands.length ? nextIndex : 0].focus();
}
} else if (event.key === 'Enter') {
if (document.activeElement && document.activeElement.classList.contains('menu-item')) {
document.activeElement.click();
}
}
}
}

function onLoad(event) {
gPendingTicker = setInterval(pendingUninstallTicker, 1000);

Expand All @@ -140,6 +172,15 @@ function onLoad(event) {
document.body.classList.remove('rendering');
});
});
chrome.tabs.query({'active': true, 'currentWindow': true}, tabs => {
if (tabs.length) {
chrome.runtime.sendMessage(
{'name': 'ListMenuCommands', 'tabId': tabs[0].id},
function(menuCommands) {
gTplData.menuCommands = menuCommands;
});
}
});
}


Expand Down
1 change: 1 addition & 0 deletions src/browser/monkey-menu.run.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
window.addEventListener('click', onClick, true);
window.addEventListener('keypress', onKeyPress, true);
window.addEventListener('DOMContentLoaded', onLoad, true);
window.addEventListener('unload', onUnload, false);

0 comments on commit c5123f7

Please sign in to comment.