style-via-api.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /* global API */// msg.js
  2. /* global addAPI */// common.js
  3. /* global isEmptyObj */// toolbox.js
  4. /* global prefs */
  5. 'use strict';
  6. /**
  7. * Uses chrome.tabs.insertCSS
  8. */
  9. (() => {
  10. const ACTIONS = {
  11. styleApply,
  12. styleDeleted,
  13. styleUpdated,
  14. styleAdded,
  15. styleReplaceAll,
  16. prefChanged,
  17. updateCount,
  18. };
  19. const NOP = new Error('NOP');
  20. const onError = () => {};
  21. /* <tabId>: Object
  22. <frameId>: Object
  23. url: String, non-enumerable
  24. <styleId>: Array of strings
  25. section code */
  26. const cache = new Map();
  27. let observingTabs = false;
  28. addAPI(/** @namespace API */ {
  29. async styleViaAPI(request) {
  30. try {
  31. const fn = ACTIONS[request.method];
  32. return fn ? fn(request, this.sender) : NOP;
  33. } catch (e) {}
  34. maybeToggleObserver();
  35. },
  36. });
  37. function updateCount(request, sender) {
  38. const {tab, frameId} = sender;
  39. if (frameId) {
  40. throw new Error('we do not count styles for frames');
  41. }
  42. const {frameStyles} = getCachedData(tab.id, frameId);
  43. API.updateIconBadge.call({sender}, Object.keys(frameStyles));
  44. }
  45. function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
  46. if (prefs.get('disableAll')) {
  47. return NOP;
  48. }
  49. const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
  50. if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
  51. return NOP;
  52. }
  53. return API.styles.getSectionsByUrl(url, id).then(sections => {
  54. const tasks = [];
  55. for (const section of Object.values(sections)) {
  56. const styleId = section.id;
  57. const code = section.code.join('\n');
  58. if (code === (frameStyles[styleId] || []).join('\n')) {
  59. continue;
  60. }
  61. frameStyles[styleId] = section.code;
  62. tasks.push(
  63. browser.tabs.insertCSS(tab.id, {
  64. code,
  65. frameId,
  66. runAt: 'document_start',
  67. matchAboutBlank: true,
  68. }).catch(onError));
  69. }
  70. if (!removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles)) {
  71. Object.defineProperty(frameStyles, 'url', {value: url, configurable: true});
  72. tabFrames[frameId] = frameStyles;
  73. cache.set(tab.id, tabFrames);
  74. }
  75. return Promise.all(tasks);
  76. })
  77. .then(() => updateCount(null, {tab, frameId}));
  78. }
  79. function styleDeleted({style: {id}}, {tab, frameId}) {
  80. const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
  81. const code = styleSections.join('\n');
  82. if (code && !duplicateCodeExists({frameStyles, id, code})) {
  83. delete frameStyles[id];
  84. removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
  85. return removeCSS(tab.id, frameId, code)
  86. .then(() => updateCount(null, {tab, frameId}));
  87. } else {
  88. return NOP;
  89. }
  90. }
  91. function styleUpdated({style}, sender) {
  92. if (!style.enabled) {
  93. return styleDeleted({style}, sender);
  94. }
  95. const {tab, frameId} = sender;
  96. const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
  97. const code = styleSections.join('\n');
  98. return styleApply(style, sender).then(code && (() => {
  99. if (!duplicateCodeExists({frameStyles, code, id: null})) {
  100. return removeCSS(tab.id, frameId, code);
  101. }
  102. }));
  103. }
  104. function styleAdded({style}, sender) {
  105. return style.enabled ? styleApply(style, sender) : NOP;
  106. }
  107. function styleReplaceAll(request, sender) {
  108. const {tab, frameId} = sender;
  109. const oldStylesCode = getFrameStylesJoined(sender);
  110. return styleApply({ignoreUrlCheck: true}, sender).then(() => {
  111. const newStylesCode = getFrameStylesJoined(sender);
  112. const tasks = oldStylesCode
  113. .filter(code => !newStylesCode.includes(code))
  114. .map(code => removeCSS(tab.id, frameId, code));
  115. return Promise.all(tasks);
  116. });
  117. }
  118. function prefChanged({prefs}, sender) {
  119. if ('disableAll' in prefs) {
  120. if (!prefs.disableAll) {
  121. return styleApply({}, sender);
  122. }
  123. const {tab, frameId} = sender;
  124. const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
  125. if (isEmptyObj(frameStyles)) {
  126. return NOP;
  127. }
  128. removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
  129. const tasks = Object.keys(frameStyles)
  130. .map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n')));
  131. return Promise.all(tasks);
  132. } else {
  133. return NOP;
  134. }
  135. }
  136. /* utilities */
  137. function maybeToggleObserver() {
  138. let method;
  139. if (!observingTabs && cache.size) {
  140. method = 'addListener';
  141. } else if (observingTabs && !cache.size) {
  142. method = 'removeListener';
  143. } else {
  144. return;
  145. }
  146. observingTabs = !observingTabs;
  147. chrome.webNavigation.onCommitted[method](onNavigationCommitted);
  148. chrome.tabs.onRemoved[method](onTabRemoved);
  149. chrome.tabs.onReplaced[method](onTabReplaced);
  150. }
  151. function onNavigationCommitted({tabId, frameId}) {
  152. if (frameId === 0) {
  153. onTabRemoved(tabId);
  154. return;
  155. }
  156. const tabFrames = cache.get(tabId);
  157. if (tabFrames && frameId in tabFrames) {
  158. delete tabFrames[frameId];
  159. if (isEmptyObj(tabFrames)) {
  160. onTabRemoved(tabId);
  161. }
  162. }
  163. }
  164. function onTabRemoved(tabId) {
  165. cache.delete(tabId);
  166. maybeToggleObserver();
  167. }
  168. function onTabReplaced(addedTabId, removedTabId) {
  169. onTabRemoved(removedTabId);
  170. }
  171. function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
  172. if (isEmptyObj(frameStyles)) {
  173. delete tabFrames[frameId];
  174. if (isEmptyObj(tabFrames)) {
  175. cache.delete(tabId);
  176. }
  177. return true;
  178. }
  179. }
  180. function getCachedData(tabId, frameId, styleId) {
  181. const tabFrames = cache.get(tabId) || {};
  182. const frameStyles = tabFrames[frameId] || {};
  183. const styleSections = styleId && frameStyles[styleId] || [];
  184. return {tabFrames, frameStyles, styleSections};
  185. }
  186. function getFrameStylesJoined({
  187. tab,
  188. frameId,
  189. frameStyles = getCachedData(tab.id, frameId).frameStyles,
  190. }) {
  191. return Object.keys(frameStyles).map(id => frameStyles[id].join('\n'));
  192. }
  193. function duplicateCodeExists({
  194. tab,
  195. frameId,
  196. frameStyles = getCachedData(tab.id, frameId).frameStyles,
  197. frameStylesCode = {},
  198. id,
  199. code = frameStylesCode[id] || frameStyles[id].join('\n'),
  200. }) {
  201. id = String(id);
  202. for (const styleId in frameStyles) {
  203. if (id !== styleId &&
  204. code === (frameStylesCode[styleId] || frameStyles[styleId].join('\n'))) {
  205. return true;
  206. }
  207. }
  208. }
  209. function removeCSS(tabId, frameId, code) {
  210. return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
  211. .catch(onError);
  212. }
  213. })();