diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2e2be027..6b33d2c9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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." diff --git a/background/background.js b/background/background.js index 2cdbc06c..7f213bc7 100644 --- a/background/background.js +++ b/background/background.js @@ -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); diff --git a/background/ext-msg.js b/background/ext-msg.js new file mode 100644 index 00000000..7f904c4b --- /dev/null +++ b/background/ext-msg.js @@ -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); + } +}; diff --git a/js/msg.js b/js/msg.js index 4b40dd0c..20e8eb0c 100644 --- a/js/msg.js +++ b/js/msg.js @@ -108,6 +108,8 @@ } return result; }, + + _onRuntimeMessage: onRuntimeMessage, }; function getExtBg() { diff --git a/js/prefs.js b/js/prefs.js index e11f5c0b..f191c224 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -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, diff --git a/options.html b/options.html index 8e9fc5f1..bd5f2789 100644 --- a/options.html +++ b/options.html @@ -16,6 +16,20 @@ + + @@ -275,6 +289,15 @@ +
+

+
+
+ +
+
+
@@ -298,6 +321,14 @@ + + + + + + + + diff --git a/options/options.css b/options/options.css index cecc91aa..ea78bb6d 100644 --- a/options/options.css +++ b/options/options.css @@ -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; diff --git a/options/options.js b/options/options.js index 3c15538f..a873b9c5 100644 --- a/options/options.js +++ b/options/options.js @@ -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([