options.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /* global API msg */// msg.js
  2. /* global prefs */
  3. /* global t */// localization.js
  4. /* global
  5. $
  6. $$
  7. $create
  8. $createLink
  9. getEventKeyName
  10. messageBoxProxy
  11. setupLivePrefs
  12. */// dom.js
  13. /* global
  14. CHROME
  15. CHROME_POPUP_BORDER_BUG
  16. FIREFOX
  17. OPERA
  18. URLS
  19. capitalize
  20. ignoreChromeError
  21. openURL
  22. */// toolbox.js
  23. 'use strict';
  24. setupLivePrefs();
  25. $$('input[min], input[max]').forEach(enforceInputRange);
  26. if (CHROME_POPUP_BORDER_BUG) {
  27. const borderOption = $('.chrome-no-popup-border');
  28. if (borderOption) {
  29. borderOption.classList.remove('chrome-no-popup-border');
  30. }
  31. }
  32. // collapse #advanced block in Chrome pre-66 (classic chrome://extensions UI)
  33. if (!FIREFOX && !OPERA && CHROME < 66) {
  34. const block = $('#advanced');
  35. $('h1', block).onclick = event => {
  36. event.preventDefault();
  37. block.classList.toggle('collapsed');
  38. const isCollapsed = block.classList.contains('collapsed');
  39. const visibleToggle = $(isCollapsed ? '.is-collapsed' : '.is-expanded', block);
  40. visibleToggle.focus();
  41. };
  42. block.classList.add('collapsible', 'collapsed');
  43. }
  44. if (FIREFOX && 'update' in (chrome.commands || {})) {
  45. $('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
  46. }
  47. // actions
  48. $('#options-close-icon').onclick = () => {
  49. top.dispatchEvent(new CustomEvent('closeOptions'));
  50. };
  51. document.onclick = e => {
  52. const target = e.target.closest('[data-cmd]');
  53. if (!target) {
  54. return;
  55. }
  56. // prevent double-triggering in case a sub-element was clicked
  57. e.stopPropagation();
  58. switch (target.dataset.cmd) {
  59. case 'open-manage':
  60. API.openManage();
  61. break;
  62. case 'check-updates':
  63. checkUpdates();
  64. break;
  65. case 'open-keyboard':
  66. if (FIREFOX) {
  67. customizeHotkeys();
  68. } else {
  69. openURL({url: URLS.configureCommands});
  70. }
  71. e.preventDefault();
  72. break;
  73. case 'reset':
  74. $$('input')
  75. .filter(input => prefs.knownKeys.includes(input.id))
  76. .forEach(input => prefs.reset(input.id));
  77. break;
  78. }
  79. };
  80. // sync to cloud
  81. (() => {
  82. const elCloud = $('.sync-options .cloud-name');
  83. const elStart = $('.sync-options .connect');
  84. const elStop = $('.sync-options .disconnect');
  85. const elSyncNow = $('.sync-options .sync-now');
  86. const elStatus = $('.sync-options .sync-status');
  87. const elLogin = $('.sync-options .sync-login');
  88. /** @type {Sync.Status} */
  89. let status = {};
  90. msg.onExtension(e => {
  91. if (e.method === 'syncStatusUpdate') {
  92. setStatus(e.status);
  93. }
  94. });
  95. API.sync.getStatus()
  96. .then(setStatus);
  97. elCloud.on('change', updateButtons);
  98. for (const [btn, fn] of [
  99. [elStart, () => API.sync.start(elCloud.value)],
  100. [elStop, API.sync.stop],
  101. [elSyncNow, API.sync.syncNow],
  102. [elLogin, async () => {
  103. await API.sync.login();
  104. await API.sync.syncNow();
  105. }],
  106. ]) {
  107. btn.on('click', e => {
  108. if (getEventKeyName(e) === 'MouseL') {
  109. fn();
  110. }
  111. });
  112. }
  113. function setStatus(newStatus) {
  114. status = newStatus;
  115. updateButtons();
  116. }
  117. function updateButtons() {
  118. const {state, STATES} = status;
  119. const isConnected = state === STATES.connected;
  120. const isDisconnected = state === STATES.disconnected;
  121. if (status.currentDriveName) {
  122. elCloud.value = status.currentDriveName;
  123. }
  124. for (const [el, enable] of [
  125. [elCloud, isDisconnected],
  126. [elStart, isDisconnected && elCloud.value !== 'none'],
  127. [elStop, isConnected && !status.syncing],
  128. [elSyncNow, isConnected && !status.syncing && status.login],
  129. ]) {
  130. el.disabled = !enable;
  131. }
  132. elStatus.textContent = getStatusText();
  133. elLogin.hidden = !isConnected || status.login;
  134. }
  135. function getStatusText() {
  136. if (status.syncing) {
  137. const {phase, loaded, total} = status.progress || {};
  138. return phase
  139. ? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) ||
  140. `${phase} ${loaded} / ${total}`
  141. : t('optionsSyncStatusSyncing');
  142. }
  143. const {state, errorMessage, STATES} = status;
  144. if (errorMessage && (state === STATES.connected || state === STATES.disconnected)) {
  145. return errorMessage;
  146. }
  147. if (state === STATES.connected && !status.login) {
  148. return t('optionsSyncStatusRelogin');
  149. }
  150. return t(`optionsSyncStatus${capitalize(state)}`, null, false) || state;
  151. }
  152. })();
  153. function checkUpdates() {
  154. let total = 0;
  155. let checked = 0;
  156. let updated = 0;
  157. const maxWidth = $('#update-progress').parentElement.clientWidth;
  158. chrome.runtime.onConnect.addListener(function onConnect(port) {
  159. if (port.name !== 'updater') return;
  160. port.onMessage.addListener(observer);
  161. chrome.runtime.onConnect.removeListener(onConnect);
  162. });
  163. API.updater.checkAllStyles({observe: true});
  164. function observer(info) {
  165. if ('count' in info) {
  166. total = info.count;
  167. document.body.classList.add('update-in-progress');
  168. } else if (info.updated) {
  169. updated++;
  170. checked++;
  171. } else if (info.error) {
  172. checked++;
  173. } else if (info.done) {
  174. document.body.classList.remove('update-in-progress');
  175. }
  176. $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
  177. $('#updates-installed').dataset.value = updated || '';
  178. }
  179. }
  180. function customizeHotkeys() {
  181. // command name -> i18n id
  182. const hotkeys = new Map([
  183. ['_execute_browser_action', 'optionsCustomizePopup'],
  184. ['openManage', 'openManage'],
  185. ['styleDisableAll', 'disableAllStyles'],
  186. ]);
  187. messageBoxProxy.show({
  188. title: t('shortcutsNote'),
  189. contents: [
  190. $create('table',
  191. [...hotkeys.entries()].map(([cmd, i18n]) =>
  192. $create('tr', [
  193. $create('td', t(i18n)),
  194. $create('td',
  195. $create('input', {
  196. id: 'hotkey.' + cmd,
  197. type: 'search',
  198. //placeholder: t('helpKeyMapHotkey'),
  199. })),
  200. ]))),
  201. ],
  202. className: 'center',
  203. buttons: [t('confirmClose')],
  204. onshow(box) {
  205. const ids = [];
  206. for (const cmd of hotkeys.keys()) {
  207. const id = 'hotkey.' + cmd;
  208. ids.push(id);
  209. $('#' + id).oninput = onInput;
  210. }
  211. setupLivePrefs(ids);
  212. $('button', box).insertAdjacentElement('beforebegin',
  213. $createLink(
  214. 'https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations',
  215. t('helpAlt')));
  216. },
  217. });
  218. function onInput() {
  219. const name = this.id.split('.')[1];
  220. const shortcut = this.value.trim();
  221. if (!shortcut) {
  222. browser.commands.reset(name).catch(ignoreChromeError);
  223. this.setCustomValidity('');
  224. return;
  225. }
  226. try {
  227. browser.commands.update({name, shortcut}).then(
  228. () => this.setCustomValidity(''),
  229. err => this.setCustomValidity(err)
  230. );
  231. } catch (err) {
  232. this.setCustomValidity(err);
  233. }
  234. }
  235. }
  236. function enforceInputRange(element) {
  237. const min = Number(element.min);
  238. const max = Number(element.max);
  239. const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true}));
  240. const onChange = ({type}) => {
  241. if (type === 'input' && element.checkValidity()) {
  242. doNotify();
  243. } else if (type === 'change' && !element.checkValidity()) {
  244. element.value = Math.max(min, Math.min(max, Number(element.value)));
  245. doNotify();
  246. }
  247. };
  248. element.on('change', onChange);
  249. element.on('input', onChange);
  250. }
  251. window.onkeydown = event => {
  252. if (getEventKeyName(event) === 'Escape') {
  253. top.dispatchEvent(new CustomEvent('closeOptions'));
  254. }
  255. };