diff --git a/background.js b/background.js index 88acd85d..1162cc08 100644 --- a/background.js +++ b/background.js @@ -1,44 +1,73 @@ /* global getDatabase, getStyles, saveStyle */ 'use strict'; -chrome.webNavigation.onBeforeNavigate.addListener(data => { - webNavigationListener(null, data); +// eslint-disable-next-line no-var +var browserCommands, contextMenus; + +// ************************************************************************* +// preload the DB and report errors +getDatabase(() => {}, (...args) => { + args.forEach(arg => 'message' in arg && console.error(arg.message)); }); -chrome.webNavigation.onCommitted.addListener(data => { - webNavigationListener('styleApply', data); -}); +// ************************************************************************* +// register all listeners +chrome.runtime.onMessage.addListener(onRuntimeMessage); -chrome.webNavigation.onHistoryStateUpdated.addListener(data => { - webNavigationListener('styleReplaceAll', data); -}); +chrome.webNavigation.onBeforeNavigate.addListener(data => + webNavigationListener(null, data)); -chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => { - webNavigationListener('styleReplaceAll', data); -}); +chrome.webNavigation.onCommitted.addListener(data => + webNavigationListener('styleApply', data)); -function webNavigationListener(method, data) { - getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { - // we can't inject chrome:// and chrome-extension:// pages - // so we'll only inform our page of the change - // and it'll retrieve the styles directly - if (method && !data.url.startsWith('chrome:') && data.tabId >= 0) { - const isOwnPage = data.url.startsWith(URLS.ownOrigin); - chrome.tabs.sendMessage( - data.tabId, - {method, styles: isOwnPage ? 'DIY' : styles}, - {frameId: data.frameId}); - } - // main page frame id is 0 - if (data.frameId == 0) { - updateIcon({id: data.tabId, url: data.url}, styles); +chrome.webNavigation.onHistoryStateUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.webNavigation.onReferenceFragmentUpdated.addListener(data => + webNavigationListener('styleReplaceAll', data)); + +chrome.tabs.onAttached.addListener((tabId, data) => { + // When an edit page gets attached or detached, remember its state + // so we can do the same to the next one to open. + chrome.tabs.get(tabId, tab => { + if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) { + chrome.windows.get(tab.windowId, {populate: true}, win => { + // If there's only one tab in this window, it's been dragged to new window + prefs.set('openEditInWindow', win.tabs.length == 1); + }); } }); +}); + +chrome.contextMenus.onClicked.addListener((info, tab) => + contextMenus[info.menuItemId].click(info, tab)); + +if ('commands' in chrome) { + // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 + chrome.commands.onCommand.addListener(command => browserCommands[command]()); } -// reset i18n cache on language change +// ************************************************************************* +// Open FAQs page once after installation to guide new users. +// Do not display it in development mode. +if (chrome.runtime.getManifest().update_url) { + const openHomepageOnInstall = ({reason}) => { + chrome.runtime.onInstalled.removeListener(openHomepageOnInstall); + if (reason == 'install') { + const version = chrome.runtime.getManifest().version; + setTimeout(openURL, 100, { + url: `http://add0n.com/stylus.html?version=${version}&type=install` + }); + } + }; + // bind for 60 seconds max and auto-unbind if it's a normal run + chrome.runtime.onInstalled.addListener(openHomepageOnInstall); + setTimeout(openHomepageOnInstall, 60e3, {reason: 'unbindme'}); +} -setTimeout(() => { +// ************************************************************************* +// reset L10N cache on UI language change +{ const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {}; const UIlang = chrome.i18n.getUILanguage(); if (browserUIlanguage != UIlang) { @@ -46,74 +75,22 @@ setTimeout(() => { browserUIlanguage: UIlang, }); } -}); - -// messaging - -chrome.runtime.onMessage.addListener(onRuntimeMessage); - -function onRuntimeMessage(request, sender, sendResponse) { - switch (request.method) { - - case 'getStyles': - getStyles(request, styles => { - sendResponse(styles); - // check if this is a main content frame style enumeration - if (request.matchUrl && !request.id - && sender && sender.tab && sender.frameId == 0 - && sender.tab.url == request.matchUrl) { - updateIcon(sender.tab, styles); - } - }); - return KEEP_CHANNEL_OPEN; - - case 'saveStyle': - saveStyle(request).then(sendResponse); - return KEEP_CHANNEL_OPEN; - - case 'healthCheck': - getDatabase( - () => sendResponse(true), - () => sendResponse(false)); - return KEEP_CHANNEL_OPEN; - - case 'prefChanged': - for (var prefName in request.prefs) { // eslint-disable-line no-var - if (prefName in contextMenus) { // eslint-disable-line no-use-before-define - chrome.contextMenus.update(prefName, { - checked: request.prefs[prefName], - }, ignoreChromeError); - } - } - break; - - case 'download': - download(request.url) - .then(sendResponse) - .catch(() => sendResponse(null)); - return KEEP_CHANNEL_OPEN; - } } -// commands (global hotkeys) - -const browserCommands = { +// ************************************************************************* +// browser commands +browserCommands = { openManage() { openURL({url: '/manage.html'}); }, - styleDisableAll(state) { - prefs.set('disableAll', - typeof state == 'boolean' ? state : !prefs.get('disableAll')); + styleDisableAll(info) { + prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); }, }; -// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 -if ('commands' in chrome) { - chrome.commands.onCommand.addListener(command => browserCommands[command]()); -} +// ************************************************************************* // context menus -// eslint-disable-next-line no-var -var contextMenus = { +contextMenus = Object.assign({ 'show-badge': { title: 'menuShowBadge', click: info => prefs.set(info.menuItemId, info.checked), @@ -126,29 +103,25 @@ var contextMenus = { title: 'openStylesManager', click: browserCommands.openManage, }, -}; - -// detect browsers without Delete by looking at the end of UA string -// Google Chrome: Safari/# -// but skip CentBrowser: Safari/# plus Shockwave Flash in plugins -// Vivaldi: Vivaldi/# -if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent) - || /Safari\/[\d.]+$/.test(navigator.userAgent) - && !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')) { - contextMenus.editDeleteText = { +}, + // detect browsers without Delete by looking at the end of UA string + /Vivaldi\/[\d.]+$/.test(navigator.userAgent) || + // Chrome and co. + /Safari\/[\d.]+$/.test(navigator.userAgent) && + // skip forks with Flash as those are likely to have the menu e.g. CentBrowser + !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash') +&& { + 'editDeleteText': { title: 'editDeleteText', contexts: ['editable'], documentUrlPatterns: [URLS.ownOrigin + 'edit*'], click: (info, tab) => { chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'}); }, - }; -} + } +}); -chrome.contextMenus.onClicked.addListener((info, tab) => - contextMenus[info.menuItemId].click(info, tab)); - -Object.keys(contextMenus).forEach(id => { +for (const id of Object.keys(contextMenus)) { const item = Object.assign({id}, contextMenus[id]); const prefValue = prefs.readOnlyValues[id]; const isBoolean = typeof prefValue == 'boolean'; @@ -162,110 +135,80 @@ Object.keys(contextMenus).forEach(id => { } delete item.click; chrome.contextMenus.create(item, ignoreChromeError); -}); +} - -// Get the DB so that any first run actions will be performed immediately -// when the background page loads. -getDatabase(() => {}, (...args) => { - args.forEach(arg => 'message' in arg && console.error(arg.message)); -}); - - -// When an edit page gets attached or detached, remember its state -// so we can do the same to the next one to open. -const editFullUrl = URLS.ownOrigin + 'edit.html'; -chrome.tabs.onAttached.addListener((tabId, data) => { - chrome.tabs.get(tabId, tabData => { - if (tabData.url.startsWith(editFullUrl)) { - chrome.windows.get(tabData.windowId, {populate: true}, win => { - // If there's only one tab in this window, it's been dragged to new window - prefs.set('openEditInWindow', win.tabs.length == 1); - }); - } - }); -}); - -// eslint-disable-next-line no-var -var codeMirrorThemes; -getCodeMirrorThemes().then(themes => { - codeMirrorThemes = themes; -}); - -// do not use prefs.get('version', null) as it might not yet be available -chrome.storage.local.get('version', prefs => { - // Open FAQs page once after installation to guide new users, - // https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160 - if (!prefs.version) { - // do not display the FAQs page in development mode - if ('update_url' in chrome.runtime.getManifest()) { - const version = chrome.runtime.getManifest().version; - chrome.storage.local.set({version}, () => { - window.setTimeout(() => { - chrome.tabs.create({ - url: `http://add0n.com/stylus.html?version=${version}&type=install` - }); - }, 3000); - }); - } - } -}); - - -injectContentScripts(); - -function injectContentScripts() { - // expand * as .*? - const wildcardAsRegExp = (s, flags) => - new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); - const contentScripts = chrome.runtime.getManifest().content_scripts; - for (const cs of contentScripts) { - cs.matches = cs.matches.map(m => ( - m == '' ? m : wildcardAsRegExp(m) - )); - } - chrome.tabs.query({}, tabs => { - for (const tab of tabs) { - for (const cs of contentScripts) { - for (const m of cs.matches) { - if ((m == '' || tab.url.match(m)) - && (!tab.url.startsWith('chrome') || tab.url == 'chrome://newtab/')) { - chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { - if (!pong) { - chrome.tabs.executeScript(tab.id, { - file: cs.js[0], - runAt: cs.run_at, - allFrames: cs.all_frames, - matchAboutBlank: cs.match_about_blank, - }, ignoreChromeError); - } - }); - // inject the content script just once - break; - } - } +Object.defineProperty(contextMenus, 'updateOnPrefChanged', { + value: changedPrefs => { + for (const id in changedPrefs) { + if (id in contextMenus) { + chrome.contextMenus.update(id, { + checked: changedPrefs[id], + }, ignoreChromeError); } } - }); + } +}); + +// ************************************************************************* +// [re]inject content scripts +{ + const NTP = 'chrome://newtab/'; + const PING = {method: 'ping'}; + const ALL_URLS = ''; + const contentScripts = chrome.runtime.getManifest().content_scripts; + // expand * as .*? + const wildcardAsRegExp = (s, flags) => new RegExp( + s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') + .replace(/\*/g, '.*?'), flags); + for (const cs of contentScripts) { + cs.matches = cs.matches.map(m => ( + m == ALL_URLS ? m : wildcardAsRegExp(m) + )); + } + + const injectCS = (cs, tabId) => { + chrome.tabs.executeScript(tabId, { + file: cs.js[0], + runAt: cs.run_at, + allFrames: cs.all_frames, + matchAboutBlank: cs.match_about_blank, + }, ignoreChromeError); + }; + + const pingCS = (cs, {id, url}) => { + cs.matches.some(match => { + if ((match == ALL_URLS || url.match(match)) + && (!url.startsWith('chrome') || url == NTP)) { + chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id)); + return true; + } + }); + }; + + chrome.tabs.query({}, tabs => + tabs.forEach(tab => + contentScripts.forEach(cs => + pingCS(cs, tab)))); } -function refreshAllTabs() { - return new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - chrome.tabs.query({}, tabs => { - const lastTab = tabs[tabs.length - 1]; - for (const tab of tabs) { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { - const message = {method: 'styleReplaceAll', styles}; - chrome.tabs.sendMessage(tab.id, message); - updateIcon(tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); +// ************************************************************************* + +function webNavigationListener(method, {url, tabId, frameId}) { + getStyles({matchUrl: url, enabled: true, asHash: true}, styles => { + if (method && !url.startsWith('chrome:') && tabId >= 0) { + chrome.tabs.sendMessage(tabId, { + method, + // ping own page so it retrieves the styles directly + styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles, + }, { + frameId + }); + } + // main page frame id is 0 + if (frameId == 0) { + updateIcon({id: tabId, url}, styles); + } }); } @@ -322,73 +265,31 @@ function updateIcon(tab, styles) { } -function getCodeMirrorThemes() { - if (!chrome.runtime.getPackageDirectoryEntry) { - return Promise.resolve([ - chrome.i18n.getMessage('defaultTheme'), - '3024-day', - '3024-night', - 'abcdef', - 'ambiance', - 'ambiance-mobile', - 'base16-dark', - 'base16-light', - 'bespin', - 'blackboard', - 'cobalt', - 'colorforth', - 'dracula', - 'duotone-dark', - 'duotone-light', - 'eclipse', - 'elegant', - 'erlang-dark', - 'hopscotch', - 'icecoder', - 'isotope', - 'lesser-dark', - 'liquibyte', - 'material', - 'mbo', - 'mdn-like', - 'midnight', - 'monokai', - 'neat', - 'neo', - 'night', - 'panda-syntax', - 'paraiso-dark', - 'paraiso-light', - 'pastel-on-dark', - 'railscasts', - 'rubyblue', - 'seti', - 'solarized', - 'the-matrix', - 'tomorrow-night-bright', - 'tomorrow-night-eighties', - 'ttcn', - 'twilight', - 'vibrant-ink', - 'xq-dark', - 'xq-light', - 'yeti', - 'zenburn', - ]); +function onRuntimeMessage(request, sender, sendResponse) { + switch (request.method) { + + case 'getStyles': + getStyles(request, sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'saveStyle': + saveStyle(request).then(sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'healthCheck': + getDatabase( + () => sendResponse(true), + () => sendResponse(false)); + return KEEP_CHANNEL_OPEN; + + case 'prefChanged': + contextMenus.updateOnPrefChanged(request.prefs); + break; + + case 'download': + download(request.url) + .then(sendResponse) + .catch(() => sendResponse(null)); + return KEEP_CHANNEL_OPEN; } - return new Promise(resolve => { - chrome.runtime.getPackageDirectoryEntry(rootDir => { - rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { - themeDir.createReader().readEntries(entries => { - resolve([ - chrome.i18n.getMessage('defaultTheme') - ].concat( - entries.filter(entry => entry.isFile) - .sort((a, b) => (a.name < b.name ? -1 : 1)) - .map(entry => entry.name.replace(/\.css$/, '')) - )); - }); - }); - }); - }); } diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index 004daad0..ce5f2032 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,4 +1,4 @@ -/* global messageBox, handleUpdate */ +/* global messageBox, handleUpdate, applyOnMessage */ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -151,7 +151,7 @@ function importFromString(jsonString) { stats.metaOnly.names.length + stats.codeOnly.names.length + stats.added.names.length; - Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => { + Promise.resolve(numChanged && refreshAllTabs()).then(() => { const report = Object.keys(stats) .filter(kind => stats[kind].names.length) .map(kind => { @@ -248,6 +248,29 @@ function importFromString(jsonString) { ? oldStyle.name + ' —> ' + newStyle.name : oldStyle.name; } + + function refreshAllTabs() { + return getActiveTab().then(activeTab => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + chrome.tabs.query({}, tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id == activeTab.id) { + applyOnMessage(message); + } else { + chrome.tabs.sendMessage(tab.id, message); + } + BG.updateIcon(tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + })); + } } diff --git a/edit.js b/edit.js index ae41418a..15f58376 100644 --- a/edit.js +++ b/edit.js @@ -47,6 +47,8 @@ new MutationObserver((mutations, observer) => { } }).observe(document, {subtree: true, childList: true}); +getCodeMirrorThemes(); + // reroute handling to nearest editor when keypress resolves to one of these commands var hotkeyRerouter = { commands: { @@ -254,14 +256,15 @@ function initCodeMirror() { return options.map(function(opt) { return ""; }).join(""); } var themeControl = document.getElementById("editor.theme"); - if (BG && BG.codeMirrorThemes) { - themeControl.innerHTML = optionsHtmlFromArray(BG.codeMirrorThemes); + const themeList = localStorage.codeMirrorThemes; + if (themeList) { + themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/)); } else { // Chrome is starting up and shows our edit.html, but the background page isn't loaded yet const theme = prefs.get("editor.theme"); themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]); - BG.getCodeMirrorThemes().then(themes => { - BG.codeMirrorThemes = themes; + getCodeMirrorThemes().then(() => { + const themes = (localStorage.codeMirrorThemes || '').split(/\s+/); themeControl.innerHTML = optionsHtmlFromArray(themes); themeControl.selectedIndex = Math.max(0, themes.indexOf(theme)); }); @@ -1868,3 +1871,78 @@ function getComputedHeight(el) { return el.getBoundingClientRect().height + parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom); } + + +function getCodeMirrorThemes() { + if (!chrome.runtime.getPackageDirectoryEntry) { + const themes = Promise.resolve([ + chrome.i18n.getMessage('defaultTheme'), + '3024-day', + '3024-night', + 'abcdef', + 'ambiance', + 'ambiance-mobile', + 'base16-dark', + 'base16-light', + 'bespin', + 'blackboard', + 'cobalt', + 'colorforth', + 'dracula', + 'duotone-dark', + 'duotone-light', + 'eclipse', + 'elegant', + 'erlang-dark', + 'hopscotch', + 'icecoder', + 'isotope', + 'lesser-dark', + 'liquibyte', + 'material', + 'mbo', + 'mdn-like', + 'midnight', + 'monokai', + 'neat', + 'neo', + 'night', + 'panda-syntax', + 'paraiso-dark', + 'paraiso-light', + 'pastel-on-dark', + 'railscasts', + 'rubyblue', + 'seti', + 'solarized', + 'the-matrix', + 'tomorrow-night-bright', + 'tomorrow-night-eighties', + 'ttcn', + 'twilight', + 'vibrant-ink', + 'xq-dark', + 'xq-light', + 'yeti', + 'zenburn', + ]); + localStorage.codeMirrorThemes = themes.join(' '); + } + return new Promise(resolve => { + chrome.runtime.getPackageDirectoryEntry(rootDir => { + rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => { + themeDir.createReader().readEntries(entries => { + const themes = [ + chrome.i18n.getMessage('defaultTheme') + ].concat( + entries.filter(entry => entry.isFile) + .sort((a, b) => (a.name < b.name ? -1 : 1)) + .map(entry => entry.name.replace(/\.css$/, '')) + ); + localStorage.codeMirrorThemes = themes.join(' '); + resolve(themes); + }); + }); + }); + }); +}