diff --git a/.eslintrc b/.eslintrc.yml similarity index 99% rename from .eslintrc rename to .eslintrc.yml index 20603487..bc2145c8 100644 --- a/.eslintrc +++ b/.eslintrc.yml @@ -31,7 +31,7 @@ rules: dot-location: [2, property] dot-notation: [0] eol-last: [2] - eqeqeq: [1, always] + eqeqeq: [1, smart] func-call-spacing: [2, never] func-name-matching: [0] func-names: [0] @@ -84,7 +84,7 @@ rules: no-empty-function: [0] no-empty-pattern: [2] no-empty: [2, {allowEmptyCatch: true}] - no-eq-null: [2] + no-eq-null: [0] no-eval: [2] no-ex-assign: [2] no-extend-native: [2] diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index b73b4418..04e6665f 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -398,11 +398,7 @@ "message": "Управление", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Прозорец за настройките", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Настройки", "description": "Go to Options UI" }, @@ -749,4 +745,4 @@ "message": "този адрес", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 965357bc..ec112682 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -723,11 +723,7 @@ "message": "Spravovat", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Možnosti rozhraní", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Možnosti", "description": "Go to Options UI" }, @@ -1318,4 +1314,4 @@ "message": "Nahrávání souboru…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/de/messages.json b/_locales/de/messages.json index f63daada..e83ee247 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -897,11 +897,7 @@ "message": "Verwalten", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Optionen", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Optionen", "description": "Go to Options UI" }, @@ -1512,4 +1508,4 @@ "message": "Lade Styles hoch...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b903a753..004fee86 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -927,11 +927,7 @@ "message": "Manage", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Options UI", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Options", "description": "Go to Options UI" }, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index d4e09bd4..39b1d226 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -897,11 +897,7 @@ "message": "Administrar", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Interfaz de opciones", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opciones", "description": "Go to Options UI" }, @@ -1524,4 +1520,4 @@ "message": "Subiendo el archivo....", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/et/messages.json b/_locales/et/messages.json index 7c103184..4442ce45 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -815,11 +815,7 @@ "message": "Halda", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Valikute liides", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Valikud", "description": "Go to Options UI" }, @@ -1426,4 +1422,4 @@ "message": "Faili üleslaadimine...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 02e37c70..0d7806f6 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -913,11 +913,7 @@ "message": "Gestion", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Paramètres d'interface graphique", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Paramètres", "description": "Go to Options UI" }, @@ -1524,4 +1520,4 @@ "message": "Envoi du fichier…", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 6c2ca738..707e641e 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -591,11 +591,7 @@ "message": "ניהול", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "אפשרויות UI", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "אפשרויות", "description": "Go to Options UI" }, @@ -969,4 +965,4 @@ "message": "הקישור הנוכחי", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 2e6ebc16..2b14d738 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -677,11 +677,7 @@ "message": "Kezelés", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "A beállítások felülete", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Beállítások", "description": "Go to Options UI" }, @@ -1252,4 +1248,4 @@ "message": "ehhez az URL-hez", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/it/messages.json b/_locales/it/messages.json index b9955937..f93a94e4 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -639,11 +639,7 @@ "message": "Gestisci gli stili installati", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Opzioni UI", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opzioni", "description": "Go to Options UI" }, @@ -1066,4 +1062,4 @@ "message": "questo URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 72b33c2b..cfdf5833 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -913,11 +913,7 @@ "message": "管理", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "オプション UI", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "オプション", "description": "Go to Options UI" }, @@ -1548,4 +1544,4 @@ "message": "スタイルをアップロード中...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index c1eaa18c..65bb1907 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -901,11 +901,7 @@ "message": "Beheren", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Opties", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opties", "description": "Go to Options UI" }, @@ -1532,4 +1528,4 @@ "message": "Bestand uploaden...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 5710a931..1be69d20 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -917,11 +917,7 @@ "message": "Zarządzaj", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Opcje interfejsu", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opcje", "description": "Go to Options UI" }, @@ -1556,4 +1552,4 @@ "message": "Wysyłanie stylów...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 04e0de3b..b23e73ba 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -241,11 +241,7 @@ "message": "Nenhum estilo instalado para este site.", "description": "Text displayed when no styles are installed for the current site" }, - "openManage": { - "message": "Gerenciar estilos instalados", - "description": "Link to open the manage page." - }, - "openOptionsPopup": { + "openOptions": { "message": "Opções", "description": "Go to Options UI" }, @@ -471,4 +467,4 @@ "message": "Enviando arquivo...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index 23bb78a3..50a2abbb 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -673,11 +673,7 @@ "message": "Gerir", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "interface de Opções", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opções", "description": "Go to Options UI" }, @@ -1244,4 +1240,4 @@ "message": "este URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index f5773c7d..d373595f 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -617,11 +617,7 @@ "message": "Managerul", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "UI cu opțiuni", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Opțiuni", "description": "Go to Options UI" }, @@ -1160,4 +1156,4 @@ "message": "acest URL", "description": "Text for link in toolbar pop-up to write a new style for the current URL" } -} \ No newline at end of file +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 83283648..1961b2df 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -921,11 +921,7 @@ "message": "Менеджер", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Настройки", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Настройки", "description": "Go to Options UI" }, @@ -1560,4 +1556,4 @@ "message": "Загрузка файла...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index c77341df..91077622 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -871,11 +871,7 @@ "message": "Hantera installerade stilar", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "Alternativ UI", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "Alternativ", "description": "Go to Options UI" }, @@ -1490,4 +1486,4 @@ "message": "Skickar filen...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 9c5256e6..dca9d656 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -917,11 +917,7 @@ "message": "管理样式", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "设置用户界面", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "设置用户界面", "description": "Go to Options UI" }, @@ -1556,4 +1552,4 @@ "message": "正在上传文件...", "description": "" } -} \ No newline at end of file +} diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index b3895479..8e83a7e7 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -917,11 +917,7 @@ "message": "管理已安裝樣式", "description": "Link to open the manage page." }, - "openOptionsManage": { - "message": "選項介面", - "description": "Go to Options UI" - }, - "openOptionsPopup": { + "openOptions": { "message": "選項", "description": "Go to Options UI" }, @@ -1556,4 +1552,4 @@ "message": "正在上傳檔案……", "description": "" } -} \ No newline at end of file +} diff --git a/background/background.js b/background/background.js index 80b4364d..1e937b12 100644 --- a/background/background.js +++ b/background/background.js @@ -1,6 +1,8 @@ /* global download prefs openURL FIREFOX CHROME VIVALDI debounce URLS ignoreChromeError getTab - styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync */ + styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync + findExistTab createTab activateTab isTabReplaceable getActiveTab */ + 'use strict'; // eslint-disable-next-line no-var @@ -28,7 +30,11 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { removeExclusion: styleManager.removeExclusion, getTabUrlPrefix() { - return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1]; + const {url} = this.sender.tab; + if (url.startsWith(URLS.ownOrigin)) { + return 'stylus'; + } + return url.match(/^([\w-]+:\/+[^/#]+)/)[1]; }, download(msg) { @@ -69,7 +75,9 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { syncStop: sync.stop, syncNow: sync.syncNow, getSyncStatus: sync.getStatus, - syncLogin: sync.login + syncLogin: sync.login, + + openManage }); // eslint-disable-next-line no-var @@ -174,9 +182,8 @@ chrome.runtime.onInstalled.addListener(({reason}) => { // ************************************************************************* // browser commands browserCommands = { - openManage() { - openURL({url: 'manage.html'}); - }, + openManage, + openOptions: () => openManage({options: true}), styleDisableAll(info) { prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); }, @@ -197,6 +204,10 @@ contextMenus = { title: 'openStylesManager', click: browserCommands.openManage, }, + 'open-options': { + title: 'openOptions', + click: browserCommands.openOptions, + }, 'editor.contextDelete': { presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'), title: 'editDeleteText', @@ -388,15 +399,55 @@ function onRuntimeMessage(msg, sender) { return fn.apply(context, msg.args); } -// FIXME: popup.js also open editor but it doesn't use this API. -function openEditor({id}) { - let url = '/edit.html'; - if (id) { - url += `?id=${id}`; +function openEditor(params) { + /* Open the editor. Activate if it is already opened + + params: { + id?: Number, + domain?: String, + 'url-prefix'?: String } - if (chrome.windows && prefs.get('openEditInWindow')) { - chrome.windows.create(Object.assign({url}, prefs.get('windowPosition'))); - } else { - openURL({url}); + */ + const searchParams = new URLSearchParams(); + for (const key in params) { + searchParams.set(key, params[key]); } + const search = searchParams.toString(); + return openURL({ + url: 'edit.html' + (search && `?${search}`), + newWindow: prefs.get('openEditInWindow'), + windowPosition: prefs.get('windowPosition'), + currentWindow: null + }); +} + +function openManage({options = false, search} = {}) { + let url = chrome.runtime.getURL('manage.html'); + if (search) { + url += `?search=${encodeURIComponent(search)}`; + } + if (options) { + url += '#stylus-options'; + } + return findExistTab({ + url, + currentWindow: null, + ignoreHash: true, + ignoreSearch: true + }) + .then(tab => { + if (tab) { + return Promise.all([ + activateTab(tab), + tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url}) + .catch(console.error) + ]); + } + return getActiveTab().then(tab => { + if (isTabReplaceable(tab, url)) { + return activateTab(tab, {url}); + } + return createTab({url}); + }); + }); } diff --git a/background/content-scripts.js b/background/content-scripts.js index 1cfecc19..d617796a 100644 --- a/background/content-scripts.js +++ b/background/content-scripts.js @@ -53,7 +53,7 @@ const contentScripts = (() => { } function injectToAllTabs() { - return queryTabs().then(tabs => { + return queryTabs({}).then(tabs => { for (const tab of tabs) { // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF if (tab.width) { diff --git a/content/apply.js b/content/apply.js index 89c3a540..7b35b1fc 100644 --- a/content/apply.js +++ b/content/apply.js @@ -202,18 +202,20 @@ const APPLY = (() => { } function applyOnMessage(request) { - if (request.method === 'ping') { - return true; - } if (STYLE_VIA_API) { if (request.method === 'urlChanged') { request.method = 'styleReplaceAll'; } - API.styleViaAPI(request); - return; + if (/^(style|updateCount)/.test(request.method)) { + API.styleViaAPI(request); + return; + } } switch (request.method) { + case 'ping': + return true; + case 'styleDeleted': styleInjector.remove(request.style.id); break; @@ -273,7 +275,11 @@ const APPLY = (() => { if (parentDomain) { return Promise.resolve(); } - return API.getTabUrlPrefix() + return msg.send({ + method: 'invokeAPI', + name: 'getTabUrlPrefix', + args: [] + }) .then(newDomain => { parentDomain = newDomain; }); diff --git a/edit/regexp-tester.js b/edit/regexp-tester.js index 348b5cb7..590714d3 100644 --- a/edit/regexp-tester.js +++ b/edit/regexp-tester.js @@ -66,7 +66,7 @@ const regExpTester = (() => { return rxData; }); const getMatchInfo = m => m && {text: m[0], pos: m.index}; - queryTabs().then(tabs => { + queryTabs({}).then(tabs => { const supported = tabs.map(tab => tab.url) .filter(url => URLS.supported(url)); const unique = [...new Set(supported).values()]; diff --git a/js/messaging.js b/js/messaging.js index 8cabad39..a0237381 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -1,6 +1,7 @@ /* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual - closeCurrentTab capitalize */ + closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */ +/* global promisify */ 'use strict'; const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); @@ -28,6 +29,7 @@ if (!CHROME && !chrome.browserAction.openPopup) { const URLS = { ownOrigin: chrome.runtime.getURL(''), + // FIXME delete? optionsUI: [ chrome.runtime.getURL('options.html'), 'chrome://extensions/?options=' + chrome.runtime.id, @@ -91,12 +93,13 @@ if (IS_BG) { // Object.defineProperty(window, 'localStorage', {value: {}}); // Object.defineProperty(window, 'sessionStorage', {value: {}}); -function queryTabs(options = {}) { - return new Promise(resolve => - chrome.tabs.query(options, tabs => - resolve(tabs))); -} - +const createTab = promisify(chrome.tabs.create.bind(chrome.tabs)); +const queryTabs = promisify(chrome.tabs.query.bind(chrome.tabs)); +const updateTab = promisify(chrome.tabs.update.bind(chrome.tabs)); +const moveTabs = promisify(chrome.tabs.move.bind(chrome.tabs)); +// FIXME: is it possible that chrome.windows is undefined? +const updateWindow = promisify(chrome.windows.update.bind(chrome.windows)); +const createWindow = promisify(chrome.windows.create.bind(chrome.windows)); function getTab(id) { return new Promise(resolve => @@ -192,6 +195,39 @@ function onTabReady(tabOrId) { }); } +function urlToMatchPattern(url, ignoreSearch) { + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns + if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) { + return undefined; + } + if (ignoreSearch) { + return [ + `${url.protocol}//${url.hostname}/${url.pathname}`, + `${url.protocol}//${url.hostname}/${url.pathname}?*` + ]; + } + // FIXME: is %2f allowed in pathname and search? + return `${url.protocol}//${url.hostname}/${url.pathname}${url.search}`; +} + +function findExistTab({url, currentWindow, ignoreHash = true, ignoreSearch = false}) { + url = new URL(url); + return queryTabs({url: urlToMatchPattern(url, ignoreSearch), currentWindow}) + // FIXME: is tab.url always normalized? + .then(tabs => tabs.find(matchTab)); + + function matchTab(tab) { + const tabUrl = new URL(tab.url); + return tabUrl.protocol === url.protocol && + tabUrl.username === url.username && + tabUrl.password === url.password && + tabUrl.hostname === url.hostname && + tabUrl.port === url.port && + tabUrl.pathname === url.pathname && + (ignoreSearch || tabUrl.search === url.search) && + (ignoreHash || tabUrl.hash === url.hash); + } +} /** * Opens a tab or activates an existing one, @@ -211,72 +247,77 @@ function onTabReady(tabOrId) { * JSONifiable data to be sent to the tab via sendMessage() * @returns {Promise} Promise that resolves to the opened/activated tab */ -function openURL({ - // https://github.com/eslint/eslint/issues/10639 - // eslint-disable-next-line no-undef - url = arguments[0], - index, - active, - currentWindow = true, -}) { - url = url.includes('://') ? url : chrome.runtime.getURL(url); - // [some] chromium forks don't handle their fake branded protocols - url = url.replace(/^(opera|vivaldi)/, 'chrome'); - // FF doesn't handle moz-extension:// URLs (bug) - // FF decodes %2F in encoded parameters (bug) - // API doesn't handle the hash-fragment part - const urlQuery = - url.startsWith('moz-extension') || - url.startsWith('chrome:') ? - undefined : - FIREFOX && url.includes('%2F') ? - url.replace(/%2F.*/, '*').replace(/#.*/, '') : - url.replace(/#.*/, ''); - - return queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch); - - function maybeSwitch(tabs = []) { - const urlWithSlash = url + '/'; - const urlFF = FIREFOX && url.replace(/%2F/g, '/'); - const tab = tabs.find(({url: u}) => u === url || u === urlFF || u === urlWithSlash); - if (!tab) { - return getActiveTab().then(maybeReplace); - } - if (index !== undefined && tab.index !== index) { - chrome.tabs.move(tab.id, {index}); - } - return activateTab(tab); +function openURL(options) { + if (typeof options === 'string') { + options = {url: options}; } + let { + url, + index, + active, + currentWindow = true, + newWindow = false, + windowPosition + } = options; - // update current NTP or about:blank - // except when 'url' is chrome:// or chrome-extension:// in incognito - function maybeReplace(tab) { - const chromeInIncognito = tab && tab.incognito && url.startsWith('chrome'); - const emptyTab = tab && URLS.emptyTab.includes(tab.url); - if (emptyTab && !chromeInIncognito) { - return new Promise(resolve => - chrome.tabs.update({url}, resolve)); - } - const options = {url, index, active}; - // FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows) - if (tab && (!FIREFOX || FIREFOX >= 57 && chrome.windows) && !chromeInIncognito) { - options.openerTabId = tab.id; - } - return new Promise(resolve => - chrome.tabs.create(options, resolve)); + if (!url.includes('://')) { + url = chrome.runtime.getURL(url); } + return findExistTab({url, currentWindow}).then(tab => { + if (tab) { + // update url if only hash is different? + // we can't update URL if !url.includes('#') since it refreshes the page + // FIXME: should we move the tab (i.e. specifying index)? + if (tab.url !== url && tab.url.split('#')[0] === url.split('#')[0] && + url.includes('#')) { + return activateTab(tab, {url, index}); + } + return activateTab(tab, {index}); + } + if (newWindow) { + return createWindow(Object.assign({url}, windowPosition)); + } + return getActiveTab().then(tab => { + if (isTabReplaceable(tab, url)) { + // don't move the tab in this case + return activateTab(tab, {url}); + } + const options = {url, index, active}; + // FF57+ supports openerTabId, but not in Android (indicated by the absence of chrome.windows) + // FIXME: is it safe to assume that the current tab is the opener? + if (tab && !tab.incognito && (!FIREFOX || FIREFOX >= 57 && chrome.windows)) { + options.openerTabId = tab.id; + } + return createTab(options); + }); + }); } +// replace empty tab (NTP or about:blank) +// except when new URL is chrome:// or chrome-extension:// and the empty tab is +// in incognito +function isTabReplaceable(tab, newUrl) { + if (!tab || !URLS.emptyTab.includes(tab.url)) { + return false; + } + // FIXME: but why? + if (tab.incognito && newUrl.startsWith('chrome')) { + return false; + } + return true; +} -function activateTab(tab) { +function activateTab(tab, {url, index} = {}) { + const options = {active: true}; + if (url) { + options.url = url; + } return Promise.all([ - new Promise(resolve => { - chrome.tabs.update(tab.id, {active: true}, resolve); - }), - chrome.windows && new Promise(resolve => { - chrome.windows.update(tab.windowId, {focused: true}, resolve); - }), - ]).then(([tab]) => tab); + updateTab(tab.id, options), + updateWindow(tab.windowId, {focused: true}), + index != null && moveTabs(tab.id, {index}) + ]) + .then(() => tab); } diff --git a/js/router.js b/js/router.js new file mode 100644 index 00000000..59566b59 --- /dev/null +++ b/js/router.js @@ -0,0 +1,99 @@ +/* global deepEqual msg */ +/* exported router */ +'use strict'; + +const router = (() => { + const buffer = []; + const watchers = []; + document.addEventListener('DOMContentLoaded', () => update()); + window.addEventListener('popstate', () => update()); + window.addEventListener('hashchange', () => update()); + msg.on(e => { + if (e.method === 'pushState' && e.url !== location.href) { + history.pushState(history.state, null, e.url); + update(); + return true; + } + }); + return {watch, updateSearch, getSearch, updateHash}; + + function watch(options, callback) { + /* Watch search params or hash and get notified on change. + + options: {search?: Array, hash?: String} + callback: (Array | Boolean) => void + + `hash` should always start with '#'. + When watching search params, callback receives a list of values. + When watching hash, callback receives a boolean. + */ + watchers.push({options, callback}); + } + + function updateSearch(key, value) { + const search = new URLSearchParams(location.search.replace(/^\?/, '')); + if (!value) { + search.delete(key); + } else { + search.set(key, value); + } + const finalSearch = search.toString(); + if (finalSearch) { + history.replaceState(history.state, null, `?${finalSearch}${location.hash}`); + } else { + history.replaceState(history.state, null, `${location.pathname}${location.hash}`); + } + update(true); + } + + function updateHash(hash) { + /* hash: String + + Send an empty string to remove the hash. + */ + if (buffer.length > 1) { + if (!hash && !buffer[buffer.length - 2].includes('#') || + hash && buffer[buffer.length - 2].endsWith(hash)) { + history.back(); + return; + } + } + if (!hash) { + hash = ' '; + } + history.pushState(history.state, null, hash); + update(); + } + + function getSearch(key) { + return new URLSearchParams(location.search.replace(/^\?/, '')).get(key); + } + + function update(replace) { + if (!buffer.length) { + buffer.push(location.href); + } else if (buffer[buffer.length - 1] === location.href) { + return; + } else if (replace) { + buffer[buffer.length - 1] = location.href; + } else if (buffer.length > 1 && buffer[buffer.length - 2] === location.href) { + buffer.pop(); + } else { + buffer.push(location.href); + } + for (const {options, callback} of watchers) { + let state; + if (options.hash) { + state = options.hash === location.hash; + } else if (options.search) { + // TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) + const search = new URLSearchParams(location.search.replace(/^\?/, '')); + state = options.search.map(key => search.get(key)); + } + if (!deepEqual(state, options.currentState)) { + options.currentState = state; + callback(state); + } + } + } +})(); diff --git a/manage.html b/manage.html index 05f2a57a..05903e91 100644 --- a/manage.html +++ b/manage.html @@ -152,6 +152,7 @@ + @@ -358,7 +359,7 @@
- + diff --git a/manage/filters.js b/manage/filters.js index b09c3193..11e11feb 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -1,4 +1,4 @@ -/* global installed messageBox sorter $ $$ $create t debounce prefs API onDOMready */ +/* global installed messageBox sorter $ $$ $create t debounce prefs API router */ /* exported filterAndAppend */ 'use strict'; @@ -9,11 +9,17 @@ const filtersSelector = { numTotal: 0, }; -// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) -const urlFilterParam = new URLSearchParams(location.search.replace(/^\?/, '')).get('url'); -if (location.search) { - history.replaceState(0, document.title, location.origin + location.pathname); -} +let initialized = false; + +router.watch({search: ['search']}, ([search]) => { + $('#search').value = search || ''; + if (!initialized) { + init(); + initialized = true; + } else { + searchStyles(); + } +}); HTMLSelectElement.prototype.adjustWidth = function () { const option0 = this.selectedOptions[0]; @@ -30,11 +36,11 @@ HTMLSelectElement.prototype.adjustWidth = function () { parent.replaceChild(this, singleSelect); }; -onDOMready().then(() => { - $('#search').oninput = searchStyles; - if (urlFilterParam) { - $('#search').value = 'url:' + urlFilterParam; - } +function init() { + $('#search').oninput = e => { + router.updateSearch('search', e.target.value); + }; + $('#search-help').onclick = event => { event.preventDefault(); messageBox({ @@ -120,6 +126,7 @@ onDOMready().then(() => { } } filterOnChange({forceRefilter: true}); + router.updateSearch('search', ''); }; // Adjust width after selects are visible @@ -131,7 +138,7 @@ onDOMready().then(() => { }); filterOnChange({forceRefilter: true}); -}); +} function filterOnChange({target: el, forceRefilter}) { @@ -271,7 +278,7 @@ function showFiltersStats() { } -function searchStyles({immediately, container}) { +function searchStyles({immediately, container} = {}) { const el = $('#search'); const query = el.value.trim(); if (query === el.lastValue && !immediately && !container) { diff --git a/manage/import-export.js b/manage/import-export.js index 327314c0..3815de8d 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -7,8 +7,12 @@ const STYLISH_DUMP_FILE_EXT = '.txt'; const STYLUS_BACKUP_FILE_EXT = '.json'; onDOMready().then(() => { - $('#file-all-styles').onclick = exportToFile; - $('#unfile-all-styles').onclick = () => { + $('#file-all-styles').onclick = event => { + event.preventDefault(); + exportToFile(); + }; + $('#unfile-all-styles').onclick = event => { + event.preventDefault(); importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); }; diff --git a/manage/manage.css b/manage/manage.css index 25d9e2d8..da65d2e0 100644 --- a/manage/manage.css +++ b/manage/manage.css @@ -1109,6 +1109,22 @@ input[id^="manage.newUI"] { -moz-osx-font-smoothing: grayscale; } +#stylus-embedded-options { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + border: 0; + z-index: 2147483647; + background-color: hsla(0, 0%, 0%, .45); + animation: fadein .25s ease-in-out; +} + +#stylus-embedded-options.fadeout { + animation: fadeout .25s ease-in-out; +} + @keyframes fadein { from { opacity: 0; diff --git a/manage/manage.js b/manage/manage.js index 741fc9bc..ddafe0fd 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -1,13 +1,13 @@ /* global messageBox getStyleWithNoCode - filterAndAppend urlFilterParam showFiltersStats + filterAndAppend showFiltersStats checkUpdate handleUpdateInstalled objectDiff configDialog sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs URLS enforceInputRange t tWordBreak formatDate getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce - scrollElementIntoView CHROME VIVALDI FIREFOX + scrollElementIntoView CHROME VIVALDI FIREFOX router */ 'use strict'; @@ -35,7 +35,8 @@ const handleEvent = {}; Promise.all([ API.getAllStyles(true), - urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}), + // FIXME: integrate this into filter.js + router.getSearch('search') && API.searchDB({query: router.getSearch('search')}), Promise.all([ onDOMready(), prefs.initializing, @@ -80,7 +81,9 @@ function onRuntimeMessage(msg) { function initGlobalEvents() { installed = $('#installed'); installed.onclick = handleEvent.entryClicked; - $('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage(); + $('#manage-options-button').onclick = () => { + router.updateHash('#stylus-options'); + }; { const btn = $('#manage-shortcuts-button'); btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands})); @@ -700,3 +703,39 @@ function highlightEditedStyle() { requestAnimationFrame(() => scrollElementIntoView(entry)); } } + + +function embedOptions() { + let options = $('#stylus-embedded-options'); + if (!options) { + options = document.createElement('iframe'); + options.id = 'stylus-embedded-options'; + options.src = '/options.html'; + document.documentElement.appendChild(options); + } + options.focus(); +} + +function unembedOptions() { + const options = $('#stylus-embedded-options'); + if (options) { + options.contentWindow.document.body.classList.add('scaleout'); + options.classList.add('fadeout'); + animateElement(options, { + className: 'fadeout', + onComplete: () => options.remove(), + }); + } +} + +router.watch({hash: '#stylus-options'}, state => { + if (state) { + embedOptions(); + } else { + unembedOptions(); + } +}); + +window.addEventListener('closeOptions', () => { + router.updateHash(''); +}); diff --git a/manifest.json b/manifest.json index e4e0b67b..19b1ba47 100644 --- a/manifest.json +++ b/manifest.json @@ -125,10 +125,6 @@ "default_popup": "popup.html" }, "default_locale": "en", - "options_ui": { - "page": "options.html", - "chrome_style": false - }, "applications": { "gecko": { "id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}", diff --git a/options.html b/options.html index 495866e4..0a02e415 100644 --- a/options.html +++ b/options.html @@ -21,8 +21,8 @@ - + @@ -33,6 +33,13 @@ + +
+
+
+ Stylus
+
+
@@ -204,7 +211,7 @@
- +
- +