Allow the API to be used by other extensions
This can be useful for example for updating / editing styles from outside of the browser. Closes #1264.
This commit is contained in:
parent
a08dd2800d
commit
b8fc4dcaa1
|
@ -1092,6 +1092,20 @@
|
|||
"optionsCustomizeUpdate": {
|
||||
"message": "Updates"
|
||||
},
|
||||
"optionsExtMessaging": {
|
||||
"message": "External control"
|
||||
},
|
||||
"optionsExtMessagingNote": {
|
||||
"message": "You can give access to Stylus to other extensions. These extensions will get full power over your styles. For advanced users only, you are left on your own to figure everything out. No backwards compatibility guaranteed. Input extension IDs, not names."
|
||||
},
|
||||
"optionsExtMessagingAdd": {
|
||||
"message": "Add",
|
||||
"description": "Label for the button to add an external messaging extension"
|
||||
},
|
||||
"optionsExtMessagingRemove": {
|
||||
"message": "Remove",
|
||||
"description": "Label for the button to remove an external messaging extension"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Options",
|
||||
"description": "Heading for options section on manage page."
|
||||
|
|
|
@ -163,6 +163,17 @@ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
|||
}
|
||||
});
|
||||
|
||||
prefs.subscribe('externals.allowedExtensionIds', (id, value) => {
|
||||
/* global onMessageExternal */// ext-msg.js
|
||||
if (value.length > 0) {
|
||||
require(['/background/ext-msg'], () => {
|
||||
chrome.runtime.onMessageExternal.addListener(onMessageExternal);
|
||||
});
|
||||
} else if (window.onMessageExternal) {
|
||||
chrome.runtime.onMessageExternal.removeListener(onMessageExternal);
|
||||
}
|
||||
}, {runNow: true});
|
||||
|
||||
msg.on((msg, sender) => {
|
||||
if (msg.method === 'invokeAPI') {
|
||||
let res = msg.path.reduce((res, name) => res && res[name], API);
|
||||
|
|
21
background/ext-msg.js
Normal file
21
background/ext-msg.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* global msg */
|
||||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
window.onMessageExternal = function ({data, target}, sender, sendResponse) {
|
||||
// Check origin
|
||||
if (!sender.id || sender.id !== chrome.runtime.id
|
||||
&& !prefs.get('externals.allowedExtensionIds').includes(sender.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedAPI =
|
||||
['openEditor', 'openManage', 'styles', 'sync', 'updater', 'usercss', 'usw'];
|
||||
// Check content
|
||||
if (target === 'extension' && data && data.method === 'invokeAPI'
|
||||
&& data.path && allowedAPI.includes(data.path[0])
|
||||
) {
|
||||
msg._onRuntimeMessage({data, target}, sender, sendResponse);
|
||||
}
|
||||
};
|
|
@ -108,6 +108,8 @@
|
|||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_onRuntimeMessage: onRuntimeMessage,
|
||||
};
|
||||
|
||||
function getExtBg() {
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
'styleViaXhr': false, // early style injection to avoid FOUC
|
||||
'patchCsp': false, // add data: and popular image hosting sites to strict CSP
|
||||
|
||||
'externals.allowedExtensionIds': [], // extensions allowed to message Stylus
|
||||
|
||||
// checkbox in style config dialog
|
||||
'config.autosave': true,
|
||||
|
||||
|
|
31
options.html
31
options.html
|
@ -16,6 +16,20 @@
|
|||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<template data-id="externals.allowedExtensionIdsItem">
|
||||
<li>
|
||||
<div class="ext-messaging-value-wrapper">
|
||||
<input data-pref-name="externals.allowedExtensionIds" spellcheck="false">
|
||||
<a class="remove-item" i18n-text="optionsExtMessagingRemove" i18-title="optionsExtMessagingRemove" tabindex="0">
|
||||
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
|
||||
</a>
|
||||
<a class="add-item" i18n-text="optionsExtMessagingAdd" i18-title="optionsExtMessagingAdd" tabindex="0">
|
||||
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<link rel="stylesheet" href="options/onoffswitch.css">
|
||||
<link rel="stylesheet" href="options/options.css">
|
||||
</head>
|
||||
|
@ -275,6 +289,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block ext-messaging-options">
|
||||
<h1 i18n-text="optionsExtMessaging"></h1>
|
||||
<div class="items">
|
||||
<div class="label" i18n-text="optionsExtMessagingNote"></div>
|
||||
<ul data-pref-name="externals.allowedExtensionIds">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="block" id="actions">
|
||||
|
@ -298,6 +321,14 @@
|
|||
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
|
||||
<path fill-rule="evenodd" d="M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-plus" viewBox="0 0 8 8">
|
||||
<path fill-rule="evenodd" d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-minus" viewBox="0 0 8 8">
|
||||
<path fill-rule="evenodd" d="M0 3v2h8v-2h-8z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<script src="options/options.js"></script>
|
||||
|
|
|
@ -352,6 +352,40 @@ html:not(.firefox):not(.opera) #updates {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.ext-messaging-options ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ext-messaging-options li {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
align-items: center;
|
||||
}
|
||||
.ext-messaging-value-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.ext-messaging-value-wrapper input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.add-item,
|
||||
.remove-item {
|
||||
font-size: 0;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-item .svg-icon,
|
||||
.remove-item .svg-icon {
|
||||
pointer-events: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.svg-inline-wrapper .svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
setupLivePrefs();
|
||||
setupRadioButtons();
|
||||
setupLists();
|
||||
$$('input[min], input[max]').forEach(enforceInputRange);
|
||||
|
||||
if (CHROME_POPUP_BORDER_BUG) {
|
||||
|
@ -219,6 +220,81 @@ function setupRadioButtons() {
|
|||
});
|
||||
}
|
||||
|
||||
function setupLists() {
|
||||
const uls = {};
|
||||
const onChange = function () {
|
||||
let newValue = getValues(this.dataset.prefName);
|
||||
if (!simpleEquals(newValue, prefs.get(this.dataset.prefName))) {
|
||||
prefs.set(this.dataset.prefName, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
for (const ul of $$('ul[data-pref-name]')) {
|
||||
const prefName = ul.dataset.prefName;
|
||||
uls[prefName] = ul;
|
||||
recreateList(prefName, prefs.get(prefName));
|
||||
}
|
||||
|
||||
prefs.subscribe(Object.keys(uls), (key, value) => {
|
||||
if (!simpleEquals(getValues(key), value)) {
|
||||
recreateList(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
function simpleEquals(ary1, ary2) {
|
||||
if (ary1.length !== ary2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < ary1.length; i++) {
|
||||
if (ary1[i] !== ary2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getValues(prefName) {
|
||||
return $$(`input[data-pref-name="${prefName}"]`, uls[prefName]).reduce(
|
||||
(result, input) => {
|
||||
if (input.value.trim() !== '') {
|
||||
result.push(input.value.trim());
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function recreateList(prefName, prefValue) {
|
||||
const ul = $(`ul[data-pref-name="${prefName}"]`);
|
||||
if (prefValue.length == 0) {
|
||||
ul.replaceChildren(createItem(prefName));
|
||||
} else {
|
||||
ul.replaceChildren(...prefValue.map(value => createItem(prefName, value)));
|
||||
}
|
||||
}
|
||||
|
||||
function createItem(prefName, value = '') {
|
||||
const it = t.template[`${prefName}Item`].cloneNode(true);
|
||||
const input = $('input[data-pref-name]', it);
|
||||
input.value = value;
|
||||
input.on('change', onChange);
|
||||
|
||||
$('.add-item', it).on('click', () => {
|
||||
it.parentElement.insertBefore(createItem(prefName), it.nextElementSibling);
|
||||
});
|
||||
$('.remove-item', it).on('click', () => {
|
||||
const input = $('input[data-pref-name]', it);
|
||||
if (it.parentElement.childElementCount == 1) {
|
||||
input.value = ''; // doesn't trigger onChange?
|
||||
} else {
|
||||
it.remove();
|
||||
}
|
||||
onChange.call(input);
|
||||
});
|
||||
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
function customizeHotkeys() {
|
||||
// command name -> i18n id
|
||||
const hotkeys = new Map([
|
||||
|
|
Loading…
Reference in New Issue
Block a user