From 728e01168b800d6e83909642558b8327c186d65e Mon Sep 17 00:00:00 2001 From: brook hong Date: Sat, 9 Nov 2024 09:19:54 +0800 Subject: [PATCH] Merge all changes for MV3 and make it buildable for both MV2 and MV3 --- config/webpack.config.js | 40 +- package.json | 2 +- src/background/chrome.js | 8 +- src/background/start.js | 243 ++++++---- src/common/utils.js | 40 ++ src/content_scripts/chrome.js | 4 +- src/content_scripts/common/api.js | 477 +++----------------- src/content_scripts/common/clipboard.js | 23 +- src/content_scripts/common/default.js | 458 ++++++++++++++++--- src/content_scripts/common/hints.js | 172 +++---- src/content_scripts/common/insert.js | 47 +- src/content_scripts/common/mode.js | 10 +- src/content_scripts/common/normal.js | 37 +- src/content_scripts/common/observer.js | 26 +- src/content_scripts/common/runtime.js | 9 +- src/content_scripts/common/utils.js | 75 ++- src/content_scripts/common/visual.js | 20 +- src/content_scripts/content.js | 152 +++---- src/content_scripts/front.js | 356 ++++++++------- src/content_scripts/options.js | 49 +- src/content_scripts/start.js | 7 +- src/content_scripts/ui/command.js | 6 - src/content_scripts/ui/frontend.js | 34 +- src/content_scripts/ui/omnibar.js | 30 +- src/manifest.json | 1 - src/pages/options.html | 11 +- src/user_scripts/index.js | 288 ++++++++++++ tests/background/start.test.js | 3 + tests/content_scripts/common/normal.test.js | 12 +- tests/content_scripts/markdown.test.js | 8 +- tests/content_scripts/ui/frontend.test.js | 9 +- tests/content_scripts/ui/omnibar.test.js | 9 +- 32 files changed, 1530 insertions(+), 1136 deletions(-) create mode 100644 src/common/utils.js create mode 100644 src/user_scripts/index.js diff --git a/config/webpack.config.js b/config/webpack.config.js index baee26586..316f63bf0 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -16,19 +16,52 @@ function modifyManifest(browser, mode, buffer) { manifest.options_ui = { page: "pages/options.html" }; - manifest.permissions.push("cookies", - "contextualIdentities"); + manifest.permissions.push("cookies"); + manifest.permissions.push("contextualIdentities"); + manifest.permissions.push(""); } else if (browser === "safari") { manifest.incognito = "split"; manifest.options_page = "pages/options.html"; + manifest.permissions.push(""); manifest.background.persistent = false; } else { + // chromium family + manifest.manifest_version = 3; manifest.permissions.push("proxy"); manifest.permissions.push("tts"); manifest.permissions.push("downloads.shelf"); - manifest.background.persistent = false; + manifest.permissions.push("favicon"); + manifest.permissions.push("userScripts"); + manifest.permissions.push("scripting"); manifest.incognito = "split"; manifest.options_page = "pages/options.html"; + manifest.background = { + "service_worker": "background.js" + }; + manifest.host_permissions = [ + "" + ]; + manifest.web_accessible_resources = [ + { + "resources": [ + "api.js", + "pages/neovim.html", + "pages/default.js", + "pages/emoji.tsv", + "pages/l10n.json", + "pages/frontend.html", + "pages/pdf_viewer.html", + "pages/shadow.css", + "pages/default.css" + ], + "matches": [ + "" + ] + } + ]; + manifest.action = manifest.browser_action; + delete manifest.browser_action; + delete manifest.content_security_policy; if (mode === "development") { manifest.key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAneIRqYRqG/0RoYzpWoyeeO8KxxvWZvIabABbeQyHQ2PFOf81j/O5J28HGAEQJ56AptKMTcTeG2qZga9B2u9k98OmRcGp8BDco6fh1vD6/x0fWfehPeub5IcEcQmCd1lBuVa8AtUqV3C+He5rS4g8dB8g8GRlSPPSiDSVNMv+iwKAk7TbM3TKz6DyFO8eCtWXr6wJCcYeJA+Mub7o8DKIHKgv8XH8+GbJGjeeIUBU7mlGlyS7ivdsG1V6D2/Ldx0O1e6sRn7f9jiC4Xy1N+zgZ7BshYbnlbwedomg1d5kuo5m4rS+8BgTchPPkhkvEs62MI4e+fmQd0oGgs7PtMSrTwIDAQAb"; @@ -54,6 +87,7 @@ module.exports = (env, argv) => { }; const moduleEntries = { 'pages/options': './src/content_scripts/options.js', + 'api': './src/user_scripts/index.js' }; const pagesCopyOptions = { ignore: [ diff --git a/package.json b/package.json index 8b7056115..b4580b2ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Surfingkeys", - "version": "1.16.1", + "version": "1.17.3", "description": "Map your keys for web surfing, expand your browser with javascript and keyboard.", "main": "background.js", "directories": { diff --git a/src/background/chrome.js b/src/background/chrome.js index ae6bbcf1f..c1e42b08d 100644 --- a/src/background/chrome.js +++ b/src/background/chrome.js @@ -1,7 +1,7 @@ import { LOG, filterByTitleOrUrl, -} from '../content_scripts/common/utils.js'; +} from '../common/utils.js'; import { _save, dictFromArray, @@ -26,6 +26,8 @@ function loadRawSettings(keys, cb, defaultSet) { cb(subset); }); } else if (localSavedAt < syncSavedAt) { + // don't sync local path + delete syncSet.localPath; extendObject(rawSet, syncSet); cb(getSubSettings(rawSet, keys)); _save(chrome.storage.local, syncSet); @@ -117,7 +119,7 @@ function getLatestHistoryItem(text, maxResults, cb) { text: "", maxResults: prefetch }, function(items) { - const filtered = filterByTitleOrUrl(items, text); + const filtered = filterByTitleOrUrl(items, text, false); results = [...results, ...filtered]; if (items.length < maxResults || results.length >= maxResults) { // all items are scanned or we have got what we want @@ -134,7 +136,7 @@ function getLatestHistoryItem(text, maxResults, cb) { function generatePassword() { const random = new Uint32Array(8); - window.crypto.getRandomValues(random); + self.crypto.getRandomValues(random); return Array.from(random).join(""); } diff --git a/src/background/start.js b/src/background/start.js index fc00180f4..d3bbbadd2 100644 --- a/src/background/start.js +++ b/src/background/start.js @@ -1,26 +1,26 @@ import { filterByTitleOrUrl, -} from '../content_scripts/common/utils.js'; +} from '../common/utils.js'; function request(url, onReady, headers, data, onException) { headers = headers || {}; - return new Promise(function(acc, rej) { - var xhr = new XMLHttpRequest(); - var method = (data !== undefined) ? "POST" : "GET"; - xhr.open(method, url); - for (var h in headers) { - xhr.setRequestHeader(h, headers[h]); - } - xhr.onload = function() { - // status from file:/// is always 0 - if (xhr.status === 200 || xhr.status === 0) { - acc(xhr.responseText); - } else { - rej(xhr.status); - } - }; - xhr.onerror = rej.bind(null, xhr); - xhr.send(data); - }).then(onReady).catch(function(exp) { + const CHARTSET_RE = /(?:charset|encoding)\s*=\s*['"]? *([\w\-]+)/i; + + fetch(url, { + method: (data !== undefined) ? "POST" : "GET", + headers, + body: data, + }).then(res => { + const cs = res.headers.get('content-type') ? res.headers.get('content-type').match(CHARTSET_RE) : []; + + return Promise.all([ + Promise.resolve(cs && cs.length > 1 ? cs[1] : "utf-8"), + res.arrayBuffer() + ]) + }).then(res => { + const decoder = new TextDecoder(res[0]); + const content = decoder.decode(res[1]); + onReady(content); + }).catch(exp => { onException && onException(exp); }); } @@ -63,7 +63,9 @@ function _save(storage, data, cb) { delete data.snippets; delete data.localPath; } - storage.set(data, cb); + if (Object.keys(data).length > 1) { + storage.set(data, cb); + } } else { if (data.localPath) { delete data.snippets; @@ -196,6 +198,8 @@ var Gist = (function() { function start(browser) { var self = {}; + const isMV3 = chrome.runtime.getManifest().manifest_version === 3; + var tabHistory = [], tabHistoryIndex = 0, chromelikeNewTabPosition = 0, @@ -304,9 +308,11 @@ function start(browser) { chrome.tabs.onRemoved.addListener(removeTab); function _setScrollPos_bg(tabId) { if (tabMessages.hasOwnProperty(tabId)) { - var message = tabMessages[tabId]; - chrome.tabs.executeScript(tabId, { - code: "_setScrollPos(" + message.scrollLeft + ", " + message.scrollTop + ")" + const message = tabMessages[tabId]; + sendTabMessage(tabId, 0, { + subject: "setScrollPos", + scrollLeft: message.scrollLeft, + scrollTop: message.scrollTop }); delete tabMessages[tabId]; } @@ -345,7 +351,6 @@ function start(browser) { changeInfo }); } - _setScrollPos_bg(tabId); }); chrome.windows.onFocusChanged.addListener(function(w) { getActiveTab(function(tab) { @@ -354,8 +359,6 @@ function start(browser) { }); chrome.tabs.onCreated.addListener(function(tab) { - _setScrollPos_bg(tab.id); - _updateTabIndices(); }); chrome.tabs.onMoved.addListener(function() { @@ -377,7 +380,6 @@ function start(browser) { historyTabAction = false; chromelikeNewTabPosition = 0; - _setScrollPos_bg(activeInfo.tabId); _updateTabIndices(); }); chrome.tabs.onDetached.addListener(function() { @@ -443,7 +445,7 @@ function start(browser) { } sendResponse(result); } - chrome.runtime.onMessage.addListener(function (_message, _sender, _sendResponse) { + function handleMessage(_message, _sender, _sendResponse) { if (self.hasOwnProperty(_message.action)) { if (_message.repeats > conf.repeatThreshold) { _message.repeats = conf.repeatThreshold; @@ -462,7 +464,20 @@ function start(browser) { } else { console.log("[unexpected runtime message] " + JSON.stringify(_message)); } - }); + } + chrome.runtime.onMessage.addListener(handleMessage); + if (isMV3) { + chrome.runtime.onUserScriptMessage.addListener((m, s, r) => { + m.fromUserScript = true; + handleMessage(m, s, r); + }); + chrome.runtime.onInstalled.addListener((e) => { + chrome.userScripts.configureWorld({ + csp: 'script-src \'self\' \'unsafe-eval\'', + messaging: true + }); + }); + } function _updateSettings(diffSettings, afterSet) { diffSettings.savedAt = new Date().getTime(); @@ -538,7 +553,7 @@ function start(browser) { loadSettings('blocklist', function(data) { var origin = ".*"; var senderOrigin = sender.origin || new URL(getSenderUrl(sender)).origin; - if (chrome.extension.getURL("/").indexOf(senderOrigin) !== 0 && senderOrigin !== "null") { + if (chrome.runtime.getURL("/").indexOf(senderOrigin) !== 0 && senderOrigin !== "null") { origin = senderOrigin; } if (data.blocklist.hasOwnProperty(origin)) { @@ -557,7 +572,7 @@ function start(browser) { }; self.toggleMouseQuery = function(message, sender, sendResponse) { loadSettings('mouseSelectToQuery', function(data) { - if (sender.tab && sender.tab.url.indexOf(chrome.extension.getURL("/")) !== 0) { + if (sender.tab && sender.tab.url.indexOf(chrome.runtime.getURL("/")) !== 0) { var mouseSelectToQuery = data.mouseSelectToQuery || []; var idx = mouseSelectToQuery.indexOf(message.origin); if (idx === -1) { @@ -570,10 +585,12 @@ function start(browser) { }); }; self.getState = function(message, sender, sendResponse) { - loadSettings(['blocklist', 'noPdfViewer'], function(data) { + loadSettings(['blocklist', 'noPdfViewer', 'proxyMode', 'proxy'], function(data) { if (sender.tab) { _response(message, sendResponse, { noPdfViewer: data.noPdfViewer, + proxyMode: data.proxyMode, + proxy: data.proxy, state: _getState(data, new URL(getSenderUrl(sender)), message.blocklistPattern, message.lurkingPattern) }); } @@ -651,7 +668,7 @@ function start(browser) { tabs = tabs.filter(function(b) { return b.url; }); - return filterByTitleOrUrl(tabs, query); + return filterByTitleOrUrl(tabs, query, false); } self.getRecentlyClosed = function(message, sender, sendResponse) { chrome.sessions.getRecentlyClosed({}, function(sessions) { @@ -1104,12 +1121,13 @@ function start(browser) { self.openLink = function(message, sender, sendResponse) { var url = normalizeURL(message.url); if (url.startsWith("javascript:")) { - chrome.tabs.executeScript(sender.tab.id, { - code: url.substr(11) + sendTabMessage(sender.tab.id, 0, { + subject: "showBanner", + message: "JavaScript URLs are not allowed in such operation." }); } else { if (message.tab.tabbed) { - if (sender.frameId !== 0 && chrome.extension.getURL("pages/frontend.html") === sender.url + if (sender.frameId !== 0 && chrome.runtime.getURL("pages/frontend.html") === sender.url || !sender.tab) { // if current call was made from Omnibar, the sender.tab may be stale, // as sender was bound when port was created. @@ -1138,6 +1156,44 @@ function start(browser) { message.url = 'view-source:' + sender.tab.url; self.openLink(message, sender, sendResponse); }; + function onFullSettingsRequested(data) { + data.isMV3 = isMV3; + data.useNeovim = browser.nvimServer && browser.nvimServer.instance; + data.isUserScriptsAvailable = isUserScriptsAvailable(); + if (isMV3) { + data.showAdvanced = data.isUserScriptsAvailable && data.showAdvanced; + } + + if (data.isUserScriptsAvailable) { + const userScriptId = "settingsSnippets"; + if (data.showAdvanced && data.snippets) { + const snippets = data.snippets; + chrome.userScripts.getScripts({ids:[userScriptId]}, (r) => { + const code = `import('./api.js').then((module) => {module.default("${chrome.runtime.getURL("/")}", (api, settings) => {${snippets}\n})});`; + const registerSettingSnippets = () => { + chrome.userScripts.register([{ + id: userScriptId, + matches: ['*://*/*'], + js: [{code}] + }]); + }; + if (r.length > 0) { + if (r[0].js[0].code !== code) { + chrome.userScripts.unregister({ids:[userScriptId]}, registerSettingSnippets); + } + } else { + registerSettingSnippets(); + } + }); + } else { + chrome.userScripts.getScripts({ids:[userScriptId]}, (r) => { + if (r.length > 0) { + chrome.userScripts.unregister({ids:[userScriptId]}); + } + }); + } + } + } self.getSettings = function(message, sender, sendResponse) { var pf = loadSettings; if (message.key === "RAW") { @@ -1146,14 +1202,26 @@ function start(browser) { } pf(message.key, function(data) { if (message.key === undefined) { - data.useNeovim = !!browser.nvimServer.instance; + onFullSettingsRequested(data); } + _response(message, sendResponse, { settings: data }); }); }; + function isUserScriptsAvailable() { + try { + if (chrome.userScripts) { + return true; + } + } catch { + return false; + } + return false; + } self.updateSettings = function(message, sender, sendResponse) { + let error = ""; if (message.scope === "snippets") { // For settings from snippets, don't broadcast the update // neither persist into storage @@ -1163,8 +1231,21 @@ function start(browser) { } } } else { - _updateAndPostSettings(message.settings); + if (message.settings.showAdvanced && isMV3) { + if (isUserScriptsAvailable()) { + chrome.userScripts.configureWorld({ + csp: 'script-src \'self\' \'unsafe-eval\'', + messaging: true + }); + _updateAndPostSettings(message.settings); + } else { + error = "Advanced mode is only available when Developer mode is turned on from chrome://extensions/."; + } + } else { + _updateAndPostSettings(message.settings); + } } + return { error }; }; self.setSurfingkeysIcon = function(message, sender, sendResponse) { let icon = "icons/48.png"; @@ -1173,7 +1254,8 @@ function start(browser) { } else if (message.status === "lurking") { icon = "icons/48-l.png"; } - chrome.browserAction.setIcon({ + const browserAction = isMV3 ? chrome.action : chrome.browserAction; + browserAction.setIcon({ path: icon, tabId: (sender.tab ? sender.tab.id : undefined) }); @@ -1186,38 +1268,45 @@ function start(browser) { }, message.headers, message.data); }; self.requestImage = function(message, sender, sendResponse) { - const img = document.createElement("img"); - img.crossOrigin = "Anonymous"; - img.src = message.url; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.height = img.naturalHeight; - canvas.width = img.naturalWidth; + fetch(message.url, { + method: "GET" + }).then(res => { + return res.blob() + }).then(blob => { + return createImageBitmap(blob) + }).then(img => { + const canvas = new OffscreenCanvas(img.width, img.height) const ctx = canvas.getContext('2d'); - - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0,0, canvas.width, canvas.height); + canvas.convertToBlob().then(blob => { + const fr = new FileReader(); + fr.onload = function(e) { + _response(message, sendResponse, { + text: e.target.result + }); + } + fr.readAsDataURL(blob); + }); + }).catch(exp => { _response(message, sendResponse, { - text: canvas.toDataURL() + text: "" }); - }; - img.onerror = (e) => { - // retry without crossOrigin - if (img.crossOrigin) { - delete img.src; - img.crossOrigin = null; - img.src = message.url; - } - }; + }); }; self.nextFrame = function(message, sender, sendResponse) { - var tid = sender.tab.id; - chrome.tabs.executeScript(tid, { - allFrames: true, - matchAboutBlank: true, - runAt: "document_start", - code: "typeof(getFrameId) === 'function' && getFrameId()" + const tid = sender.tab.id; + chrome.scripting.executeScript({ + target: { + allFrames: true, + tabId: tid, + }, + func: () => { + return typeof(getFrameId) === 'function' ? getFrameId() : 0; + }, }, function(framesInTab) { - framesInTab = framesInTab.filter(function(frameId) { + framesInTab = framesInTab.map((res) => { + return res.result; + }).filter((frameId) => { return frameId; }); @@ -1346,21 +1435,10 @@ function start(browser) { self.download = function(message, sender, sendResponse) { chrome.downloads.download({ url: message.url, saveAs: message.saveAs }); }; - self.executeScript = function(message, sender, sendResponse) { - chrome.tabs.executeScript(sender.tab.id, { - frameId: sender.frameId, - code: message.code, - matchAboutBlank: true, - file: message.file - }, function(result) { - _response(message, sendResponse, { - response: result - }); - }); - }; self.tabURLAccessed = function(message, sender, sendResponse) { if (sender.tab) { var tabId = sender.tab.id; + _setScrollPos_bg(tabId); if (!tabURLs.hasOwnProperty(tabId)) { tabURLs[tabId] = {}; } @@ -1650,17 +1728,6 @@ function start(browser) { return {requestHeaders: details.requestHeaders}; } - self.setUserAgent = function (message, sender, sendResponse) { - if (message.userAgent) { - userAgent = message.userAgent; - chrome.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, { - urls: [""] - }, ["blocking", "requestHeaders"]); - } else { - chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); - } - chrome.tabs.reload(sender.tab.id); - }; self.writeClipboard = function (message, sender, sendResponse) { navigator.clipboard.writeText(message.text) }; diff --git a/src/common/utils.js b/src/common/utils.js new file mode 100644 index 000000000..b7a07df4f --- /dev/null +++ b/src/common/utils.js @@ -0,0 +1,40 @@ +function LOG(level, msg) { + // To turn on all levels: chrome.storage.local.set({"logLevels": ["log", "warn", "error"]}) + chrome.storage.local.get(["logLevels"], (r) => { + const logLevels = r && r.logLevels || ["error"]; + if (["log", "warn", "error"].indexOf(level) !== -1 && logLevels.indexOf(level) !== -1) { + console[level](msg); + } + }); +} + +function regexFromString(str, caseSensitive, highlight) { + var rxp = null; + const flags = caseSensitive ? "g" : "gi"; + str = str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); + if (highlight) { + rxp = new RegExp(str.replace(/\s+/, "\|"), flags); + } else { + var words = str.split(/\s+/).map(function(w) { + return `(?=.*${w})`; + }).join(''); + rxp = new RegExp(`^${words}.*$`, flags); + } + return rxp; +} + +function filterByTitleOrUrl(urls, query, caseSensitive) { + if (query && query.length) { + var rxp = regexFromString(query, caseSensitive, false); + urls = urls.filter(function(b) { + return rxp.test(b.title) || rxp.test(b.url); + }); + } + return urls; +} + +export { + LOG, + filterByTitleOrUrl, + regexFromString, +} diff --git a/src/content_scripts/chrome.js b/src/content_scripts/chrome.js index f35962d4b..588842d67 100644 --- a/src/content_scripts/chrome.js +++ b/src/content_scripts/chrome.js @@ -5,7 +5,7 @@ import { dispatchSKEvent, runtime, RUNTIME } from './common/runtime.js'; import { start } from './content.js'; function usePdfViewer() { - window.location.replace(chrome.extension.getURL("/pages/pdf_viewer.html") + "?r=" + document.URL); + window.location.replace(chrome.runtime.getURL("/pages/pdf_viewer.html") + "?r=" + document.URL); } function readText(text, options) { @@ -43,7 +43,7 @@ function readText(text, options) { } showPopup(updated); } else if (res.ttsEvent.type === "end") { - dispatchSKEvent('hidePopup'); + dispatchSKEvent("front", ['hidePopup']); } } if (onEnd && (res.ttsEvent.type === "end" || res.ttsEvent.type === "interrupted")) { diff --git a/src/content_scripts/common/api.js b/src/content_scripts/common/api.js index e7e75ad2f..63af9a6dd 100644 --- a/src/content_scripts/common/api.js +++ b/src/content_scripts/common/api.js @@ -1,27 +1,17 @@ -import { RUNTIME, dispatchSKEvent, runtime } from './runtime.js'; +import { dispatchSKEvent } from './runtime.js'; import Trie from './trie'; import Mode from './mode'; import KeyboardUtils from './keyboardUtils'; import { LOG, - actionWithSelectionPreserved, +} from '../../common/utils.js'; +import { constructSearchURL, - getBrowserName, - getClickableElements, - getCssSelectorsOfEditable, - getRealEdit, - getTextNodePos, - getWordUnderCursor, - htmlEncode, - insertJS, - isElementPartiallyInViewport, + initSKFunctionListener, + isInUIFrame, mapInMode, parseAnnotation, - setSanitizedContent, - showBanner, - showPopup, tabOpenLink, - toggleQuote, } from './utils.js'; function createAPI(clipboard, insert, normal, hints, visual, front, browser) { @@ -145,7 +135,7 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { } else { if (!mapInMode(normal, new_keystroke, old_keystroke, new_annotation) && old_keystroke in Mode.specialKeys) { Mode.specialKeys[old_keystroke].push(new_keystroke); - dispatchSKEvent('addMapkey', ["Mode", new_keystroke, old_keystroke]); + dispatchSKEvent("front", ['addMapkey', "Mode", new_keystroke, old_keystroke]); } else { LOG("warn", `${old_keystroke} not found in normal mode.`); } @@ -250,7 +240,7 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { */ function cmap(new_keystroke, old_keystroke, domain, new_annotation) { if (_isDomainApplicable(domain)) { - dispatchSKEvent('addMapkey', ["Omnibar", new_keystroke, old_keystroke]); + dispatchSKEvent("front", ['addMapkey', "Omnibar", new_keystroke, old_keystroke]); } } @@ -300,58 +290,6 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { } } - /** - * Map the key sequence `lhs` to `rhs` for mode `ctx` in ACE editor. - * - * @param {string} lhs a key sequence to replace - * @param {string} rhs a key sequence to be replaced - * @param {string} ctx a mode such as `insert`, `normal`. - * - * @example aceVimMap('J', ':bn', 'normal'); - */ - function aceVimMap(lhs, rhs, ctx) { - dispatchSKEvent('addVimMap', [lhs, rhs, ctx]); - } - - /** - * Add map key in ACE editor. - * - * @param {object} objects multiple objects to define key map in ACE, see more from [ace/keyboard/vim.js](https://github.com/ajaxorg/ace/blob/ec450c03b51aba3724cf90bb133708078d1f3de6/lib/ace/keyboard/vim.js#L927-L1099) - * - * @example - * addVimMapKey( - * { - * keys: 'n', - * type: 'motion', - * motion: 'moveByCharacters', - * motionArgs: { - * forward: false - * } - * }, - * - * { - * keys: 'e', - * type: 'motion', - * motion: 'moveByLines', - * motionArgs: { - * forward: true, - * linewise: true - * } - * } - * ); - */ - function addVimMapKey() { - dispatchSKEvent('addVimKeyMap', Array.from(arguments)); - } - - function _addSearchAlias(alias, prompt, url, suggestionURL, listSuggestion, options) { - front.addSearchAlias(alias, prompt, url, suggestionURL, listSuggestion, options); - } - - function _removeSearchAlias(alias) { - front.removeSearchAlias(alias); - } - /** * Add a search engine alias into Omnibar. * @@ -373,7 +311,9 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { * }); */ function addSearchAlias(alias, prompt, search_url, search_leader_key, suggestion_url, callback_to_parse_suggestion, only_this_site_key, options) { - _addSearchAlias(alias, prompt, search_url, suggestion_url, callback_to_parse_suggestion, options); + if (!isInUIFrame() && front.addSearchAlias) { + front.addSearchAlias(alias, prompt, search_url, suggestion_url, callback_to_parse_suggestion, options); + } const skipMaps = options?.skipMaps ?? false if (skipMaps) { return @@ -418,7 +358,9 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { * removeSearchAlias('d'); */ function removeSearchAlias(alias, search_leader_key, only_this_site_key) { - _removeSearchAlias(alias); + if (!isInUIFrame()) { + front.removeSearchAlias(alias); + } unmap((search_leader_key || 's') + alias); unmap('o' + alias); vunmap((search_leader_key || 's') + alias); @@ -459,369 +401,62 @@ function createAPI(clipboard, insert, normal, hints, visual, front, browser) { }); } - function getFormData(form, format) { - var formData = new FormData(form); - if (format === "json") { - var obj = {}; - - formData.forEach(function (value, key) { - if (obj.hasOwnProperty(key)) { - if (value.length) { - var p = obj[key]; - if (p.constructor.name === "Array") { - p.push(value); - } else { - obj[key] = []; - if (p.length) { - obj[key].push(p); - } - obj[key].push(value); - } - } - } else { - obj[key] = value; - } - }); - - return obj; - } else { - return new URLSearchParams(formData).toString(); - } - } - - mapkey('[[', '#1Click on the previous link on current page', hints.previousPage); - mapkey(']]', '#1Click on the next link on current page', hints.nextPage); - mapkey('T', '#3Choose a tab', function() { - front.chooseTab(); - }); - mapkey('?', '#0Show usage', function() { - front.showUsage(); - }); - mapkey('Q', '#8Open omnibar for word translation', function() { - front.openOmniquery({query: getWordUnderCursor(), style: "opacity: 0.8;"}); - }); - imapkey("", '#15Toggle quotes in an input element', toggleQuote); - function openVim(useNeovim) { - var element = getRealEdit(); - element.blur(); - insert.exit(); - front.showEditor(element, null, null, useNeovim); - } - imapkey('', '#15Open vim editor for current input', function() { - openVim(false); - }); - const browserName = getBrowserName(); - if (browserName === "Chrome") { - imapkey('', '#15Open neovim for current input', function() { - openVim(true); - }); - mapkey(';s', 'Toggle PDF viewer from SurfingKeys', function() { - var pdfUrl = window.location.href; - if (pdfUrl.indexOf(chrome.extension.getURL("/pages/pdf_viewer.html")) === 0) { - pdfUrl = window.location.search.substr(3); - chrome.storage.local.set({"noPdfViewer": 1}, function() { - window.location.replace(pdfUrl); - }); - } else { - if (document.querySelector("EMBED") && document.querySelector("EMBED").getAttribute("type") === "application/pdf") { - chrome.storage.local.remove("noPdfViewer", function() { - window.location.replace(pdfUrl); - }); - } else { - chrome.storage.local.get("noPdfViewer", function(resp) { - if(!resp.noPdfViewer) { - chrome.storage.local.set({"noPdfViewer": 1}, function() { - showBanner("PDF viewer disabled."); - }); - } else { - chrome.storage.local.remove("noPdfViewer", function() { - showBanner("PDF viewer enabled."); - }); - } - }); - } - } - }); - } - - mapkey(";ql", '#0Show last action', function() { - showPopup(htmlEncode(runtime.conf.lastKeys.map(function(k) { - return KeyboardUtils.decodeKeystroke(k); - }).join(' → '))); - }, {repeatIgnore: true}); - - mapkey('gi', '#1Go to the first edit box', function() { - hints.createInputLayer(); - }); - mapkey('i', '#1Go to edit box', function() { - hints.create(getCssSelectorsOfEditable(), hints.dispatchMouseClick); - }); - mapkey('I', '#1Go to edit box with vim editor', function() { - hints.create(getCssSelectorsOfEditable(), function(element) { - front.showEditor(element); - }); - }); - - mapkey('zv', '#9Enter visual mode, and select whole element', function() { - visual.toggle("z"); - }); - mapkey('yv', '#7Yank text of an element', function() { - hints.create(runtime.conf.textAnchorPat, function (element) { - clipboard.write(element[1] === 0 ? element[0].data.trim() : element[2].trim()); - }); - }); - mapkey('ymv', '#7Yank text of multiple elements', function() { - var textToYank = []; - hints.create(runtime.conf.textAnchorPat, function (element) { - textToYank.push(element[1] === 0 ? element[0].data.trim() : element[2].trim()); - clipboard.write(textToYank.join('\n')); - }, { multipleHits: true }); - }); - - mapkey('V', '#9Restore visual mode', function() { - visual.restore(); - }); - mapkey('*', '#9Find selected text in current page', function() { - visual.star(); - visual.toggle(); - }); - - vmapkey('', '#9Backward 20 lines', function() { - visual.feedkeys('20k'); - }); - vmapkey('', '#9Forward 20 lines', function() { - visual.feedkeys('20j'); - }); - - mapkey('m', '#10Add current URL to vim-like marks', normal.addVIMark); - mapkey("'", '#10Jump to vim-like mark', normal.jumpVIMark); - mapkey("", '#10Jump to vim-like mark in new tab.', function(mark) { - normal.jumpVIMark(mark); - }); - - mapkey('w', '#2Switch frames', function() { - // ensure frontend ready so that ui related actions can be available in iframes. - dispatchSKEvent('ensureFrontEnd'); - if (window !== top || !hints.create("iframe", function(element) { - element.scrollIntoView({ - behavior: 'auto', - block: 'center', - inline: 'center' - }); - normal.highlightElement(element); - element.contentWindow.focus(); - })) { - normal.rotateFrame(); - } - }); - - mapkey('yg', '#7Capture current page', function() { - front.toggleStatus(false); - setTimeout(function() { - RUNTIME('captureVisibleTab', null, function(response) { - front.toggleStatus(true); - showPopup("".format(response.dataUrl)); - }); - }, 500); - }); - - mapkey('gu', '#4Go up one path in the URL', function() { - var pathname = location.pathname; - if (pathname.length > 1) { - pathname = pathname.endsWith('/') ? pathname.substr(0, pathname.length - 1) : pathname; - var last = pathname.lastIndexOf('/'), repeats = RUNTIME.repeats; - RUNTIME.repeats = 1; - while (repeats-- > 1) { - var p = pathname.lastIndexOf('/', last - 1); - if (p === -1) { - break; - } else { - last = p; - } - } - pathname = pathname.substr(0, last); - } - window.location.href = location.origin + pathname; - }); - - mapkey(';m', '#1mouse out last element', function() { - hints.mouseoutLastElement(); - }); - - mapkey(';pp', '#7Paste html on current page', function() { - clipboard.read(function(response) { - document.documentElement.removeAttributes(); - document.body.removeAttributes(); - setSanitizedContent(document.head, "" + new Date() +" updated by Surfingkeys"); - setSanitizedContent(document.body, response.data); - }); - }); - - function openGoogleTranslate() { - if (window.getSelection().toString()) { - searchSelectedWith('https://translate.google.com/?hl=en#auto/en/', false, false, ''); - } else { - tabOpenLink("https://translate.google.com/translate?js=n&sl=auto&tl=zh-CN&u=" + window.location.href); - } - } - mapkey(';t', 'Translate selected text with google', openGoogleTranslate); - vmapkey('t', '#9Translate selected text with google', openGoogleTranslate); - - mapkey('O', '#1Open detected links from text', function() { - hints.create(runtime.conf.clickablePat, function(element) { - window.location.assign(element[2]); - }, {statusLine: "Open detected links from text"}); - }); - - mapkey(".", '#0Repeat last action', function() { - // lastKeys in format: [,(\t)*], examples - // ['se'] - // ['f', 'Hints\tBA'] - const lastKeys = runtime.conf.lastKeys; - normal.feedkeys(lastKeys[0]); - var modeKeys = lastKeys.slice(1); - for (var i = 0; i < modeKeys.length; i++) { - var modeKey = modeKeys[i].split('\t'); - if (modeKey[0] === 'Hints') { - function closureWrapper() { - var hintKeys = modeKey[1]; - return function() { - hints.feedkeys(hintKeys); - }; - } - setTimeout(closureWrapper(), 120 + i*100); - } - } - }, {repeatIgnore: true}); - - mapkey("f", '#1Open a link, press SHIFT to flip overlapped hints, hold SPACE to hide hints', function() { - hints.create("", hints.dispatchMouseClick); - }, {repeatIgnore: true}); - - mapkey("v", '#9Toggle visual mode', function() { - visual.toggle(); - }, {repeatIgnore: true}); - - mapkey("n", '#9Next found text', function() { - visual.next(false); - }, {repeatIgnore: true}); - - mapkey("N", '#9Previous found text', function() { - visual.next(true); - }, {repeatIgnore: true}); - - mapkey(";fs", '#1Display hints to focus scrollable elements', function() { - hints.create(normal.refreshScrollableElements(), hints.dispatchMouseClick); - }); - - vmapkey("q", '#9Translate word under cursor', function() { - var w = getWordUnderCursor(); - browser.readText(w); - var b = visual.getCursorPixelPos(); - front.performInlineQuery(w, { - top: b.top, - left: b.left, - height: b.height, - width: b.width - }, function(pos, queryResult) { - dispatchSKEvent('showBubble', [pos, queryResult, true]); - }); - }); - - function getSentence(textNode, offset) { - var sentence = ""; - - actionWithSelectionPreserved(function(sel) { - sel.setPosition(textNode, offset); - sel.modify("extend", "backward", "sentence"); - sel.collapseToStart(); - sel.modify("extend", "forward", "sentence"); - - sentence = sel.toString(); - }); - - return sentence.replace(/\n/g, ''); - } - - mapkey("cq", '#7Query word with Hints', function() { - hints.create(runtime.conf.textAnchorPat, function (element) { - var word = element[2].trim().replace(/[^A-z].*$/, ""); - var b = getTextNodePos(element[0], element[1], element[2].length); - if (document.dictEnabled !== undefined) { - if (document.dictEnabled) { - window.postMessage({dictorium_data: { - type: "OpenDictoriumQuery", - word: word, - sentence: getSentence(element[0], element[1]), - pos: b, - source: window.location.href - }}); - } - } else { - front.performInlineQuery(word, { - top: b.top, - left: b.left, - height: b.height, - width: b.width - }, function (pos, queryResult) { - dispatchSKEvent('showBubble', [pos, queryResult, false]); - }); - } - }); - }); - - return { - RUNTIME, - aceVimMap, - addVimMapKey, + initSKFunctionListener("api", { addSearchAlias, - cmap, imap, - imapkey, - insertJS, - isElementPartiallyInViewport, - getBrowserName, - getClickableElements, - getFormData, - lmap, map, + lmap, unmap, unmapAllExcept, iunmap, vunmap, - mapkey, - readText: browser.readText, removeSearchAlias, searchSelectedWith, - tabOpenLink, - vmap, - vmapkey, - Clipboard: clipboard, - Normal: { - feedkeys: normal.feedkeys, - jumpVIMark: normal.jumpVIMark, - passThrough: normal.passThrough, - scroll: normal.scroll, + "clipboard:write": clipboard.write, + "clipboard:read": () => { + clipboard.read((resp) => { + dispatchSKEvent('user', ["onClipboardRead", resp]); + }); }, - Hints: { - click: hints.click, - create: hints.create, - dispatchMouseClick: hints.dispatchMouseClick, - style: hints.style, - setNumeric: hints.setNumeric, - setCharacters: hints.setCharacters, + "hints:click": hints.click, + "hints:create": hints.create, + "hints:setCharacters": hints.setCharacters, + "hints:setNumeric": hints.setNumeric, + "hints:style": hints.style, + "front:registerInlineQuery": front.registerInlineQuery, + "front:openOmnibar": front.openOmnibar, + "normal:feedkeys": normal.feedkeys, + "normal:jumpVIMark": normal.jumpVIMark, + "normal:passThrough": normal.passThrough, + "normal:scroll": normal.scroll, + "visual:style": visual.style, + mapkey: (keys, annotation, options) => { + mapkey(keys, annotation, () => { + dispatchSKEvent('user', ["callUserFunction", `normal:${keys}`]); + }, options); }, - Visual: { - style: visual.style, + imapkey: (keys, annotation, options) => { + imapkey(keys, annotation, () => { + dispatchSKEvent('user', ["callUserFunction", `insert:${keys}`]); + }, options); }, - Front: { - openOmnibar: front.openOmnibar, - registerInlineQuery: front.registerInlineQuery, - showEditor: front.showEditor, - getUsage: front.getUsage, - showBanner, - showPopup, + vmapkey: (keys, annotation, options) => { + vmapkey(keys, annotation, () => { + dispatchSKEvent('user', ["callUserFunction", `visual:${keys}`]); + }, options); }, + }); + return { + Clipboard: clipboard, + addSearchAlias, + cmap, + map, + mapkey, + imapkey, + readText: browser.readText, + vmapkey, + removeSearchAlias, + searchSelectedWith, }; } diff --git a/src/content_scripts/common/clipboard.js b/src/content_scripts/common/clipboard.js index 385ab1baf..51d0ca3ed 100644 --- a/src/content_scripts/common/clipboard.js +++ b/src/content_scripts/common/clipboard.js @@ -1,7 +1,6 @@ import { RUNTIME } from './runtime.js'; import { actionWithSelectionPreserved, - insertJS, getBrowserName, setSanitizedContent, showBanner, @@ -84,22 +83,14 @@ function createClipboard() { }; // navigator.clipboard.writeText does not work on http site, and in chrome's background script. if (getBrowserName() === "Chrome") { - insertJS(function() { - window.oncopy = document.oncopy; - document.oncopy = null; - }, function() { - clipboardActionWithSelectionPreserved(function() { - holder.value = text; - holder.select(); - document.execCommand('copy'); - holder.value = ''; - }); - insertJS(function() { - document.oncopy = window.oncopy; - delete window.oncopy; - }); - cb(); + clipboardActionWithSelectionPreserved(function() { + holder.value = text; + console.log(text); + holder.select(); + document.execCommand('copy'); + holder.value = ''; }); + cb(); } else { // works for Firefox and Safari now. RUNTIME("writeClipboard", { text }); diff --git a/src/content_scripts/common/default.js b/src/content_scripts/common/default.js index 8ac7eeec3..907f345f0 100644 --- a/src/content_scripts/common/default.js +++ b/src/content_scripts/common/default.js @@ -1,20 +1,323 @@ -module.exports = function(api) { +import { RUNTIME, dispatchSKEvent, runtime } from './runtime.js'; +import KeyboardUtils from './keyboardUtils'; +import { + actionWithSelectionPreserved, + getBrowserName, + getCssSelectorsOfEditable, + getRealEdit, + getTextNodePos, + getWordUnderCursor, + htmlEncode, + setSanitizedContent, + showBanner, + showPopup, + tabOpenLink, + toggleQuote, +} from './utils.js'; + +export default function(api, clipboard, insert, normal, hints, visual, front) { const { addSearchAlias, cmap, - getBrowserName, - getFormData, map, mapkey, + imapkey, readText, - tabOpenLink, vmapkey, - Clipboard, - Hints, - Front, - RUNTIME + searchSelectedWith, } = api; + mapkey('[[', '#1Click on the previous link on current page', hints.previousPage); + mapkey(']]', '#1Click on the next link on current page', hints.nextPage); + mapkey('T', '#3Choose a tab', function() { + front.chooseTab(); + }); + mapkey('?', '#0Show usage', function() { + front.showUsage(); + }); + mapkey('Q', '#8Open omnibar for word translation', function() { + front.openOmniquery({query: getWordUnderCursor(), style: "opacity: 0.8;"}); + }); + imapkey("", '#15Toggle quotes in an input element', toggleQuote); + function openVim(useNeovim) { + var element = getRealEdit(); + element.blur(); + insert.exit(); + front.showEditor(element, null, null, useNeovim); + } + imapkey('', '#15Open vim editor for current input', function() { + openVim(false); + }); + const browserName = getBrowserName(); + if (browserName === "Chrome") { + imapkey('', '#15Open neovim for current input', function() { + openVim(true); + }); + mapkey(';s', 'Toggle PDF viewer from SurfingKeys', function() { + var pdfUrl = window.location.href; + if (pdfUrl.indexOf(chrome.runtime.getURL("/pages/pdf_viewer.html")) === 0) { + pdfUrl = window.location.search.substr(3); + RUNTIME('updateSettings', { + settings: { + "noPdfViewer": 1 + } + }, (resp) => { + window.location.replace(pdfUrl); + }); + } else { + if (document.querySelector("EMBED") && document.querySelector("EMBED").getAttribute("type") === "application/pdf") { + RUNTIME('updateSettings', { + settings: { + "noPdfViewer": 0 + } + }, (resp) => { + window.location.replace(pdfUrl); + }); + } else { + RUNTIME('getSettings', { + key: 'noPdfViewer' + }, function(resp) { + const info = resp.settings.noPdfViewer ? "PDF viewer enabled." : "PDF viewer disabled."; + RUNTIME('updateSettings', { + settings: { + "noPdfViewer": !resp.settings.noPdfViewer + } + }, (r) => { + showBanner(info); + }); + }); + } + } + }); + } + + mapkey(";ql", '#0Show last action', function() { + showPopup(htmlEncode(runtime.conf.lastKeys.map(function(k) { + return KeyboardUtils.decodeKeystroke(k); + }).join(' → '))); + }, {repeatIgnore: true}); + + mapkey('gi', '#1Go to the first edit box', function() { + hints.createInputLayer(); + }); + mapkey('i', '#1Go to edit box', function() { + hints.create(getCssSelectorsOfEditable(), hints.dispatchMouseClick); + }); + mapkey('I', '#1Go to edit box with vim editor', function() { + hints.create(getCssSelectorsOfEditable(), function(element) { + front.showEditor(element); + }); + }); + + mapkey('zv', '#9Enter visual mode, and select whole element', function() { + visual.toggle("z"); + }); + mapkey('yv', '#7Yank text of an element', function() { + hints.create(runtime.conf.textAnchorPat, function (element) { + clipboard.write(element[1] === 0 ? element[0].data.trim() : element[2].trim()); + }); + }); + mapkey('ymv', '#7Yank text of multiple elements', function() { + var textToYank = []; + hints.create(runtime.conf.textAnchorPat, function (element) { + textToYank.push(element[1] === 0 ? element[0].data.trim() : element[2].trim()); + clipboard.write(textToYank.join('\n')); + }, { multipleHits: true }); + }); + + mapkey('V', '#9Restore visual mode', function() { + visual.restore(); + }); + mapkey('*', '#9Find selected text in current page', function() { + visual.star(); + visual.toggle(); + }); + + vmapkey('', '#9Backward 20 lines', function() { + visual.feedkeys('20k'); + }); + vmapkey('', '#9Forward 20 lines', function() { + visual.feedkeys('20j'); + }); + + mapkey('m', '#10Add current URL to vim-like marks', normal.addVIMark); + mapkey("'", '#10Jump to vim-like mark', normal.jumpVIMark); + mapkey("", '#10Jump to vim-like mark in new tab.', function(mark) { + normal.jumpVIMark(mark); + }); + + mapkey('w', '#2Switch frames', function() { + // ensure frontend ready so that ui related actions can be available in iframes. + dispatchSKEvent('ensureFrontEnd'); + if (window !== top || !hints.create("iframe", function(element) { + element.scrollIntoView({ + behavior: 'auto', + block: 'center', + inline: 'center' + }); + normal.highlightElement(element); + element.contentWindow.focus(); + })) { + normal.rotateFrame(); + } + }); + + mapkey('yg', '#7Capture current page', function() { + front.toggleStatus(false); + setTimeout(function() { + RUNTIME('captureVisibleTab', null, function(response) { + front.toggleStatus(true); + showPopup("".format(response.dataUrl)); + }); + }, 500); + }); + + mapkey('gu', '#4Go up one path in the URL', function() { + var pathname = location.pathname; + if (pathname.length > 1) { + pathname = pathname.endsWith('/') ? pathname.substr(0, pathname.length - 1) : pathname; + var last = pathname.lastIndexOf('/'), repeats = RUNTIME.repeats; + RUNTIME.repeats = 1; + while (repeats-- > 1) { + var p = pathname.lastIndexOf('/', last - 1); + if (p === -1) { + break; + } else { + last = p; + } + } + pathname = pathname.substr(0, last); + } + window.location.href = location.origin + pathname; + }); + + mapkey(';m', '#1mouse out last element', function() { + hints.mouseoutLastElement(); + }); + + mapkey(';pp', '#7Paste html on current page', function() { + clipboard.read(function(response) { + document.documentElement.removeAttributes(); + document.body.removeAttributes(); + setSanitizedContent(document.head, "" + new Date() +" updated by Surfingkeys"); + setSanitizedContent(document.body, response.data); + }); + }); + + function openGoogleTranslate() { + if (window.getSelection().toString()) { + searchSelectedWith('https://translate.google.com/?hl=en#auto/en/', false, false, ''); + } else { + tabOpenLink("https://translate.google.com/translate?js=n&sl=auto&tl=zh-CN&u=" + window.location.href); + } + } + mapkey(';t', 'Translate selected text with google', openGoogleTranslate); + vmapkey('t', '#9Translate selected text with google', openGoogleTranslate); + + mapkey('O', '#1Open detected links from text', function() { + hints.create(runtime.conf.clickablePat, function(element) { + window.location.assign(element[2]); + }, {statusLine: "Open detected links from text"}); + }); + + mapkey(".", '#0Repeat last action', function() { + // lastKeys in format: [,(\t)*], examples + // ['se'] + // ['f', 'Hints\tBA'] + const lastKeys = runtime.conf.lastKeys; + normal.feedkeys(lastKeys[0]); + var modeKeys = lastKeys.slice(1); + for (var i = 0; i < modeKeys.length; i++) { + var modeKey = modeKeys[i].split('\t'); + if (modeKey[0] === 'Hints') { + function closureWrapper() { + var hintKeys = modeKey[1]; + return function() { + hints.feedkeys(hintKeys); + }; + } + setTimeout(closureWrapper(), 120 + i*100); + } + } + }, {repeatIgnore: true}); + + mapkey("f", '#1Open a link, press SHIFT to flip overlapped hints, hold SPACE to hide hints', function() { + hints.create("", hints.dispatchMouseClick); + }, {repeatIgnore: true}); + + mapkey("v", '#9Toggle visual mode', function() { + visual.toggle(); + }, {repeatIgnore: true}); + + mapkey("n", '#9Next found text', function() { + visual.next(false); + }, {repeatIgnore: true}); + + mapkey("N", '#9Previous found text', function() { + visual.next(true); + }, {repeatIgnore: true}); + + mapkey(";fs", '#1Display hints to focus scrollable elements', function() { + hints.create(normal.refreshScrollableElements(), hints.dispatchMouseClick); + }); + + vmapkey("q", '#9Translate word under cursor', function() { + var w = getWordUnderCursor(); + browser.readText(w); + var b = visual.getCursorPixelPos(); + front.performInlineQuery(w, { + top: b.top, + left: b.left, + height: b.height, + width: b.width + }, function(pos, queryResult) { + dispatchSKEvent("front", ['showBubble', pos, queryResult, true]); + }); + }); + + function getSentence(textNode, offset) { + var sentence = ""; + + actionWithSelectionPreserved(function(sel) { + sel.setPosition(textNode, offset); + sel.modify("extend", "backward", "sentence"); + sel.collapseToStart(); + sel.modify("extend", "forward", "sentence"); + + sentence = sel.toString(); + }); + + return sentence.replace(/\n/g, ''); + } + + mapkey("cq", '#7Query word with Hints', function() { + hints.create(runtime.conf.textAnchorPat, function (element) { + var word = element[2].trim().replace(/[^A-z].*$/, ""); + var b = getTextNodePos(element[0], element[1], element[2].length); + if (document.dictEnabled !== undefined) { + if (document.dictEnabled) { + window.postMessage({dictorium_data: { + type: "OpenDictoriumQuery", + word: word, + sentence: getSentence(element[0], element[1]), + pos: b, + source: window.location.href + }}); + } + } else { + front.performInlineQuery(word, { + top: b.top, + left: b.left, + height: b.height, + width: b.width + }, function (pos, queryResult) { + dispatchSKEvent("front", ['showBubble', pos, queryResult, false]); + }); + } + }); + }); + + map('g0', ':feedkeys 99E', 0, "#3Go to the first tab"); map('g$', ':feedkeys 99R', 0, "#3Go to the last tab"); mapkey('zr', '#3zoom reset', function() { @@ -47,38 +350,38 @@ module.exports = function(api) { }); map('u', 'e'); mapkey('af', '#1Open a link in active new tab', function() { - Hints.create("", Hints.dispatchMouseClick, {tabbed: true, active: true}); + hints.create("", hints.dispatchMouseClick, {tabbed: true, active: true}); }); mapkey('gf', '#1Open a link in non-active new tab', function() { - Hints.create("", Hints.dispatchMouseClick, {tabbed: true, active: false}); + hints.create("", hints.dispatchMouseClick, {tabbed: true, active: false}); }); mapkey('cf', '#1Open multiple links in a new tab', function() { - Hints.create("", Hints.dispatchMouseClick, {multipleHits: true}); + hints.create("", hints.dispatchMouseClick, {multipleHits: true}); }); map('C', 'gf'); mapkey('', '#1Mouse over elements.', function() { - Hints.create("", (element, event) => { + hints.create("", (element, event) => { if (chrome.surfingkeys) { const r = element.getClientRects()[0]; chrome.surfingkeys.sendMouseEvent(2, Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2), 0); } else { - Hints.dispatchMouseClick(element, event); + hints.dispatchMouseClick(element, event); } }, {mouseEvents: ["mouseover"]}); }); mapkey('', '#1Mouse out elements.', function() { - Hints.create("", Hints.dispatchMouseClick, {mouseEvents: ["mouseout"]}); + hints.create("", hints.dispatchMouseClick, {mouseEvents: ["mouseout"]}); }); mapkey('ya', '#7Copy a link URL to the clipboard', function() { - Hints.create('*[href]', function(element) { - Clipboard.write(element.href); + hints.create('*[href]', function(element) { + clipboard.write(element.href); }); }); mapkey('yma', '#7Copy multiple link URLs to the clipboard', function() { var linksToYank = []; - Hints.create('*[href]', function(element) { + hints.create('*[href]', function(element) { linksToYank.push(element.href); - Clipboard.write(linksToYank.join('\n')); + clipboard.write(linksToYank.join('\n')); }, {multipleHits: true}); }); function getTableColumnHeads() { @@ -92,16 +395,16 @@ module.exports = function(api) { return tds; } mapkey('yc', '#7Copy a column of a table', function() { - Hints.create(getTableColumnHeads(), function(element) { + hints.create(getTableColumnHeads(), function(element) { var column = Array.from(element.closest("table").querySelectorAll("tr")).map(function(tr) { return tr.children.length > element.cellIndex ? tr.children[element.cellIndex].innerText : ""; }); - Clipboard.write(column.join("\n")); + clipboard.write(column.join("\n")); }); }); mapkey('ymc', '#7Copy multiple columns of a table', function() { var rows = null; - Hints.create(getTableColumnHeads(), function(element) { + hints.create(getTableColumnHeads(), function(element) { var column = Array.from(element.closest("table").querySelectorAll("tr")).map(function(tr) { return tr.children.length > element.cellIndex ? tr.children[element.cellIndex].innerText : ""; }); @@ -112,12 +415,12 @@ module.exports = function(api) { rows[i] += "\t" + c; }); } - Clipboard.write(rows.join("\n")); + clipboard.write(rows.join("\n")); }, {multipleHits: true}); }); mapkey('yq', '#7Copy pre text', function() { - Hints.create("pre", function(element) { - Clipboard.write(element.innerText); + hints.create("pre", function(element) { + clipboard.write(element.innerText); }); }); @@ -127,7 +430,7 @@ module.exports = function(api) { cmap('', ''); cmap('', ''); mapkey('q', '#1Click on an Image or a button', function() { - Hints.create("img, button", Hints.dispatchMouseClick); + hints.create("img, button", hints.dispatchMouseClick); }); mapkey('', '#3pin/unpin current tab', function() { RUNTIME("togglePinTab"); @@ -177,17 +480,17 @@ module.exports = function(api) { }); mapkey('H', '#8Open opened URL in current tab', function() { - Front.openOmnibar({type: "TabURLs"}); + front.openOmnibar({type: "TabURLs"}); }); mapkey('om', '#8Open URL from vim-like marks', function() { - Front.openOmnibar({type: "VIMarks"}); + front.openOmnibar({type: "VIMarks"}); }); mapkey(':', '#8Open commands', function() { - Front.openOmnibar({type: "Commands"}); + front.openOmnibar({type: "Commands"}); }); mapkey('yi', '#7Yank text of an input', function() { - Hints.create("input, textarea, select", function(element) { - Clipboard.write(element.value); + hints.create("input, textarea, select", function(element) { + clipboard.write(element.value); }); }); mapkey('x', '#3Close current tab', function() { @@ -200,7 +503,7 @@ module.exports = function(api) { if (window.getSelection().toString()) { tabOpenLink(window.getSelection().toString()); } else { - Clipboard.read(function(response) { + clipboard.read(function(response) { tabOpenLink(response.data); }); } @@ -210,17 +513,17 @@ module.exports = function(api) { }); mapkey('ys', "#7Copy current page's source", function() { var aa = document.documentElement.cloneNode(true); - Clipboard.write(aa.outerHTML); + clipboard.write(aa.outerHTML); }); mapkey('yj', "#7Copy current settings", function() { RUNTIME('getSettings', { key: "RAW" }, function(response) { - Clipboard.write(JSON.stringify(response.settings, null, 4)); + clipboard.write(JSON.stringify(response.settings, null, 4)); }); }); mapkey(';pj', "#7Restore settings data from clipboard", function() { - Clipboard.read(function(response) { + clipboard.read(function(response) { RUNTIME('updateSettings', { settings: JSON.parse(response.data.trim()) }); @@ -234,30 +537,60 @@ module.exports = function(api) { }); mapkey('yy', "#7Copy current page's URL", function() { var url = window.location.href; - if (url.indexOf(chrome.extension.getURL("/pages/pdf_viewer.html")) === 0) { + if (url.indexOf(chrome.runtime.getURL("/pages/pdf_viewer.html")) === 0) { url = window.location.search.substr(3); } - Clipboard.write(url); + clipboard.write(url); }); mapkey('yY', "#7Copy all tabs's url", function() { RUNTIME('getTabs', null, function (response) { - Clipboard.write(response.tabs.map(tab => tab.url).join('\n')); + clipboard.write(response.tabs.map(tab => tab.url).join('\n')); }); }); mapkey('yh', "#7Copy current page's host", function() { var url = new URL(window.location.href); - Clipboard.write(url.host); + clipboard.write(url.host); }); mapkey('yl', "#7Copy current page's title", function() { - Clipboard.write(document.title); + clipboard.write(document.title); }); mapkey('yQ', '#7Copy all query history of OmniQuery.', function() { RUNTIME('getSettings', { key: 'OmniQueryHistory' }, function(response) { - Clipboard.write(response.settings.OmniQueryHistory.join("\n")); + clipboard.write(response.settings.OmniQueryHistory.join("\n")); }); }); + + function getFormData(form, format) { + var formData = new FormData(form); + if (format === "json") { + var obj = {}; + + formData.forEach(function (value, key) { + if (obj.hasOwnProperty(key)) { + if (value.length) { + var p = obj[key]; + if (p.constructor.name === "Array") { + p.push(value); + } else { + obj[key] = []; + if (p.length) { + obj[key].push(p); + } + obj[key].push(value); + } + } + } else { + obj[key] = value; + } + }); + + return obj; + } else { + return new URLSearchParams(formData).toString(); + } + } function generateFormKey(form) { return (form.method || "get") + "::" + new URL(form.action).pathname; } @@ -266,12 +599,12 @@ module.exports = function(api) { document.querySelectorAll('form').forEach(function(form) { fd[generateFormKey(form)] = getFormData(form, "json"); }); - Clipboard.write(JSON.stringify(fd, null, 4)); + clipboard.write(JSON.stringify(fd, null, 4)); }); mapkey(';pf', '#7Fill form with data from yf', function() { - Hints.create('form', function(element, event) { + hints.create('form', function(element, event) { var formKey = generateFormKey(element); - Clipboard.read(function(response) { + clipboard.read(function(response) { var forms = JSON.parse(response.data.trim()); if (forms.hasOwnProperty(formKey)) { var fd = forms[formKey]; @@ -299,7 +632,7 @@ module.exports = function(api) { } }); } else { - Front.showBanner("No form data found for your selection from clipboard."); + showBanner("No form data found for your selection from clipboard."); } }); }); @@ -311,7 +644,7 @@ module.exports = function(api) { fd[(form.method || "get") + "::" + form.action] = getFormData(form); aa.push(fd); }); - Clipboard.write(JSON.stringify(aa, null, 4)); + clipboard.write(JSON.stringify(aa, null, 4)); }); mapkey('g?', '#4Reload current page without query string(all parts after question mark)', function() { @@ -345,12 +678,12 @@ module.exports = function(api) { tabOpenLink("/pages/options.html"); }); mapkey(';u', '#4Edit current URL with vim editor, and open in new tab', function() { - Front.showEditor(window.location.href, function(data) { + front.showEditor(window.location.href, function(data) { tabOpenLink(data); }, 'url'); }); mapkey(';U', '#4Edit current URL with vim editor, and reload', function() { - Front.showEditor(window.location.href, function(data) { + front.showEditor(window.location.href, function(data) { window.location.href = data; }, 'url'); }); @@ -369,6 +702,7 @@ module.exports = function(api) { var res = response.text.match(/,s:\[("[^\]]+")]}/); return res ? res[1].replace(/"/g, '').split(",") : []; }); + addSearchAlias('e', 'wikipedia', 'https://en.wikipedia.org/wiki/', 's', 'https://en.wikipedia.org/w/api.php?action=opensearch&format=json&formatversion=2&namespace=0&limit=40&search=', function(response) { return JSON.parse(response.text)[1]; }); @@ -413,11 +747,11 @@ module.exports = function(api) { RUNTIME('getSettings', { key: ['proxyMode', 'proxy', 'autoproxy_hosts'] }, function(response) { - Clipboard.write(JSON.stringify(response.settings, null, 4)); + clipboard.write(JSON.stringify(response.settings, null, 4)); }); }); mapkey(';ap', '#13Apply proxy info from clipboard', function() { - Clipboard.read(function(response) { + clipboard.read(function(response) { var proxyConf = JSON.parse(response.data); RUNTIME('updateProxy', { operation: 'set', @@ -434,7 +768,7 @@ module.exports = function(api) { map(';ps', ':setProxyMode system', 0, '#13set proxy mode `system`'); map(';pc', ':setProxyMode clear', 0, '#13set proxy mode `clear`'); mapkey('gr', '#14Read selected text or text from clipboard', function() { - Clipboard.read(function(response) { + clipboard.read(function(response) { readText(window.getSelection().toString() || response.data, {verbose: true}); }); }); @@ -479,35 +813,35 @@ module.exports = function(api) { if (!getBrowserName().startsWith("Safari")) { mapkey('t', '#8Open a URL', function() { - Front.openOmnibar({type: "URLs"}); + front.openOmnibar({type: "URLs"}); }); mapkey('go', '#8Open a URL in current tab', function() { - Front.openOmnibar({type: "URLs", tabbed: false}); + front.openOmnibar({type: "URLs", tabbed: false}); }); mapkey('ox', '#8Open recently closed URL', function() { - Front.openOmnibar({type: "RecentlyClosed"}); + front.openOmnibar({type: "RecentlyClosed"}); }); mapkey('X', '#3Restore closed tab', function() { RUNTIME("openLast"); }); mapkey('b', '#8Open a bookmark', function() { - Front.openOmnibar(({type: "Bookmarks"})); + front.openOmnibar(({type: "Bookmarks"})); }); mapkey('ab', '#8Bookmark current page to selected folder', function() { var page = { url: window.location.href, title: document.title }; - Front.openOmnibar(({type: "AddBookmark", extra: page})); + front.openOmnibar(({type: "AddBookmark", extra: page})); }); mapkey('oh', '#8Open URL from history', function() { - Front.openOmnibar({type: "History"}); + front.openOmnibar({type: "History"}); }); mapkey('W', '#3Move current tab to another window', function() { - Front.openOmnibar(({type: "Windows"})); + front.openOmnibar(({type: "Windows"})); }); mapkey(';gt', '#3Gather filtered tabs into current window', function() { - Front.openOmnibar({type: "Tabs", extra: { + front.openOmnibar({type: "Tabs", extra: { action: "gather" }}); }); @@ -531,7 +865,7 @@ module.exports = function(api) { var items = response.downloads.map(function(o) { return o.url; }); - Clipboard.write(items.join(',')); + clipboard.write(items.join(',')); }); }); mapkey('gs', '#12View page source', function() { @@ -541,7 +875,7 @@ module.exports = function(api) { tabOpenLink("/pages/markdown.html"); }); mapkey(';di', '#1Download image', function() { - Hints.create('img', function(element) { + hints.create('img', function(element) { RUNTIME('download', { url: element.src }); @@ -557,11 +891,11 @@ module.exports = function(api) { }); mapkey(';yh', '#14Yank histories', function() { RUNTIME('getHistory', {}, function(response) { - Clipboard.write(response.history.map(h => h.url).join("\n")); + clipboard.write(response.history.map(h => h.url).join("\n")); }); }); mapkey(';ph', '#14Put histories from clipboard', function() { - Clipboard.read(function(response) { + clipboard.read(function(response) { RUNTIME('addHistories', {history: response.data.split("\n")}); }); }); diff --git a/src/content_scripts/common/hints.js b/src/content_scripts/common/hints.js index 3edf1a284..dae718888 100644 --- a/src/content_scripts/common/hints.js +++ b/src/content_scripts/common/hints.js @@ -10,10 +10,10 @@ import { getBrowserName, getClickableElements, getCssSelectorsOfEditable, - getElements, getRealRect, getTextNodePos, getVisibleElements, + initSKFunctionListener, isEditable, isElementClickable, refreshHints, @@ -171,11 +171,76 @@ div.hint-scrollable { } }); + /** + * The default `onHintKey` implementation. + * + * @param {HTMLElement} element the element for which the pressed hint is targeted. + * @name Hints.dispatchMouseClick + * @see Hints.create + * + * @example + * mapkey('q', 'click on images', function() { + * Hints.create("div.media_box img", Hints.dispatchMouseClick); + * }, {domain: /weibo.com/i}); + */ + self.dispatchMouseClick = function(element) { + if (isEditable(element)) { + self.exit(); + normal.passFocus(true); + element.focus(); + insert.enter(element); + } else { + if (!behaviours.multipleHits) { + self.exit(); + } + var tabbed = behaviours.tabbed, active = behaviours.active; + if (behaviours.multipleHits) { + const href = element.getAttribute('href'); + if (href !== null && href !== "#") { + tabbed = true; + active = false; + } + } + + if (shiftKey && runtime.conf.hintShiftNonActive) { + tabbed = true; + active = false; + } else if (shiftKey && getBrowserName() === "Firefox") { + // mouseButton does not work for firefox in mouse event. + tabbed = true; + active = true; + } + + flashPressedLink(element,() => { + if (tabbed) { + RUNTIME("openLink", { + tab: { + tabbed: tabbed, + active: active + }, + url: getHref(element) + }); + } else { + self.mouseoutLastElement(); + dispatchMouseEvent(element, behaviours.mouseEvents, shiftKey); + dispatchSKEvent("observer", ['turnOn']); + lastMouseTarget = element; + } + + if (behaviours.multipleHits) { + setTimeout(resetHints, 300); + } + }); + } + element.classList.remove("surfingkeys--hints--clicking"); + }; + + const MOUSE_EVENTS = ['mouseover', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'focus', 'focusin']; var prefix = "", textFilter = "", lastMouseTarget = null, behaviours = { - mouseEvents: ['mouseover', 'mousedown', 'mouseup', 'click'] + mouseEvents: MOUSE_EVENTS }, holder = createElementWithContent('section', '', {style: "display: block; opacity: 1;"}), shiftKey = false; @@ -203,7 +268,11 @@ div.hint-scrollable { const hintState = refreshHints(hints, prefix); if (hintState.matched) { normal.appendKeysForRepeat("Hints", prefix); - _onHintKey(hintState.matched); + if (typeof(_onHintKey) === 'function') { + _onHintKey(hintState.matched); + } else { + dispatchSKEvent('user', ["onHintClicked"], hintState.matched); + } if (behaviours.multipleHits) { prefix = ""; refreshHints(hints, prefix); @@ -248,7 +317,7 @@ div.hint-scrollable { behaviours = { active: true, tabbed: false, - mouseEvents: ['mouseover', 'mousedown', 'mouseup', 'click'], + mouseEvents: ['mouseover', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'focus', 'focusin'], multipleHits: false }; setSanitizedContent(holder, ""); @@ -274,12 +343,6 @@ div.hint-scrollable { } } - function onScrollStarted(evt) { - setSanitizedContent(holder, ""); - holder.remove(); - prefix = ""; - } - function resetHints() { if (!document.documentElement.contains(hintsHost)) { return; @@ -376,18 +439,17 @@ div.hint-scrollable { } }; - document.addEventListener("surfingkeys:topBoundaryHit", self.previousPage); - document.addEventListener("surfingkeys:bottomBoundaryHit", self.nextPage); - - self.onEnter = function() { - document.addEventListener("surfingkeys:scrollStarted", onScrollStarted); - document.addEventListener("surfingkeys:scrollDone", resetHints); - }; - - self.onExit = function() { - document.removeEventListener("surfingkeys:scrollStarted", onScrollStarted); - document.removeEventListener("surfingkeys:scrollDone", resetHints); - }; + initSKFunctionListener("hints", { + scrollStarted: () => { + setSanitizedContent(holder, ""); + holder.remove(); + prefix = ""; + }, + scrollDone: resetHints, + topBoundaryHit: self.previousPage, + bottomBoundaryHit: self.nextPage, + dispatchMouseClick: self.dispatchMouseClick, + }, true); self.genLabels = function(total) { let chars = characters.toUpperCase(); @@ -475,7 +537,7 @@ div.hint-scrollable { attrs = Object.assign({ active: true, tabbed: false, - mouseEvents: ['mouseover', 'mousedown', 'mouseup', 'click'], + mouseEvents: MOUSE_EVENTS, multipleHits: false, filterInvisible: true }, attrs || {}); @@ -499,7 +561,7 @@ div.hint-scrollable { attrs = Object.assign({ active: true, tabbed: false, - mouseEvents: ['mouseover', 'mousedown', 'mouseup', 'click'], + mouseEvents: MOUSE_EVENTS, multipleHits: false }, attrs || {}); for (var attr in attrs) { @@ -727,68 +789,6 @@ div.hint-scrollable { return found > 0; }; - /** - * The default `onHintKey` implementation. - * - * @param {HTMLElement} element the element for which the pressed hint is targeted. - * @name Hints.dispatchMouseClick - * @see Hints.create - * - * @example - * mapkey('q', 'click on images', function() { - * Hints.create("div.media_box img", Hints.dispatchMouseClick); - * }, {domain: /weibo.com/i}); - */ - self.dispatchMouseClick = function(element, event) { - if (isEditable(element)) { - self.exit(); - normal.passFocus(true); - element.focus(); - insert.enter(element); - } else { - if (!behaviours.multipleHits) { - self.exit(); - } - var tabbed = behaviours.tabbed, active = behaviours.active; - if (behaviours.multipleHits) { - const href = element.getAttribute('href'); - if (href !== null && href !== "#") { - tabbed = true; - active = false; - } - } - - if (shiftKey && runtime.conf.hintShiftNonActive) { - tabbed = true; - active = false; - } else if (shiftKey && getBrowserName() === "Firefox") { - // mouseButton does not work for firefox in mouse event. - tabbed = true; - active = true; - } - - flashPressedLink(element,() => { - if (tabbed) { - RUNTIME("openLink", { - tab: { - tabbed: tabbed, - active: active - }, - url: getHref(element) - }); - } else { - self.mouseoutLastElement(); - dispatchMouseEvent(element, behaviours.mouseEvents, shiftKey); - dispatchSKEvent('turnOnDOMObserver'); - lastMouseTarget = element; - } - - if (behaviours.multipleHits) { - setTimeout(resetHints, 300); - } - }); - } - }; self.mouseoutLastElement = function() { if (lastMouseTarget) { dispatchMouseEvent(lastMouseTarget, ['mouseout'], false); diff --git a/src/content_scripts/common/insert.js b/src/content_scripts/common/insert.js index e8ae89c56..f16c93b1e 100644 --- a/src/content_scripts/common/insert.js +++ b/src/content_scripts/common/insert.js @@ -176,32 +176,29 @@ function createInsert() { _emojiList, _emojiPending = -1; - document.addEventListener("surfingkeys:userSettingsLoaded", function(evt) { - if (runtime.conf.enableEmojiInsertion) { - self.mappings.add(":", { - annotation: "Input emoji", - feature_group: 15, - stopPropagation: function() { - return false; - }, - code: function() { - var element = getRealEdit(); - if (element.selectionStart !== undefined) { - _emojiPending = element.selectionStart + 1; - } else { - _emojiPending = document.getSelection().focusOffset + 1; - } - fetch(chrome.extension.getURL("pages/emoji.tsv")) - .then(res => Promise.all([res.text()])) - .then(res => { - _emojiList = res[0].split("\n"); - listEmoji(); - }); + self.enableEmojiInsertion = () => { + self.mappings.add(":", { + annotation: "Input emoji", + feature_group: 15, + stopPropagation: function() { + return false; + }, + code: function() { + var element = getRealEdit(); + if (element.selectionStart !== undefined) { + _emojiPending = element.selectionStart + 1; + } else { + _emojiPending = document.getSelection().focusOffset + 1; } - }); - } - }); - + fetch(chrome.runtime.getURL("pages/emoji.tsv")) + .then(res => Promise.all([res.text()])) + .then(res => { + _emojiList = res[0].split("\n"); + listEmoji(); + }); + } + }); + }; function listEmoji() { var input = getRealEdit(), query = "", isInput = true; diff --git a/src/content_scripts/common/mode.js b/src/content_scripts/common/mode.js index 5217282da..60bdbf271 100644 --- a/src/content_scripts/common/mode.js +++ b/src/content_scripts/common/mode.js @@ -240,7 +240,7 @@ Mode.showStatus = function() { sl += " - frame: " + pathname[pathname.length - 1]; } } - dispatchSKEvent('showStatus', [[sl]]); + dispatchSKEvent("front", ['showStatus', [sl]]); } }; @@ -249,7 +249,7 @@ Mode.finish = function (mode) { if (mode.map_node !== mode.mappings || mode.pendingMap != null || mode.repeats) { mode.map_node = mode.mappings; mode.pendingMap = null; - mode.isTrustedEvent && dispatchSKEvent('hideKeystroke'); + mode.isTrustedEvent && dispatchSKEvent("front", ['hideKeystroke']); if (mode.repeats) { mode.repeats = ""; } @@ -288,7 +288,7 @@ Mode.handleMapKey = function(event, onNoMatched) { ) { // reset only after target action executed or cancelled this.repeats += key; - this.isTrustedEvent && dispatchSKEvent('showKeystroke', [key, this]); + this.isTrustedEvent && dispatchSKEvent("front", ['showKeystroke', key, this]); event.sk_stopPropagation = true; } else { var last = this.map_node; @@ -303,7 +303,7 @@ Mode.handleMapKey = function(event, onNoMatched) { if (code.length) { // bound function needs arguments this.pendingMap = code; - this.isTrustedEvent && dispatchSKEvent('showKeystroke', [key, this]); + this.isTrustedEvent && dispatchSKEvent("front", ['showKeystroke', key, this]); event.sk_stopPropagation = true; } else { this.setLastKeys && this.setLastKeys(this.map_node.meta.word); @@ -317,7 +317,7 @@ Mode.handleMapKey = function(event, onNoMatched) { actionDone = Mode.finish(thisMode); } } else { - this.isTrustedEvent && dispatchSKEvent('showKeystroke', [key, this]); + this.isTrustedEvent && dispatchSKEvent("front", ['showKeystroke', key, this]); event.sk_stopPropagation = true; } } diff --git a/src/content_scripts/common/normal.js b/src/content_scripts/common/normal.js index 912ba3658..458a840ac 100644 --- a/src/content_scripts/common/normal.js +++ b/src/content_scripts/common/normal.js @@ -6,7 +6,6 @@ import { getRealEdit, isEditable, isElementClickable, - isElementDrawn, isElementPartiallyInViewport, isInUIFrame, mapInMode, @@ -175,7 +174,7 @@ function createNormal(insert) { var _once = false; self.addEventListener('keydown', function(event) { var realTarget = getRealEdit(event); - if (isEditable(realTarget) && isElementDrawn(realTarget) && event.isTrusted) { + if (isEditable(realTarget) && event.isTrusted) { if (Mode.isSpecialKeyOf("", event.sk_keyName)) { realTarget.blur(); insert.exit(); @@ -282,9 +281,9 @@ function createNormal(insert) { }); self.toggleBlocklist = function() { - if (document.location.href.indexOf(chrome.extension.getURL("/")) !== 0) { + if (document.location.href.indexOf(chrome.runtime.getURL("/")) !== 0) { RUNTIME('toggleBlocklist', { - blocklistPattern: (runtime.conf.blocklistPattern ? runtime.conf.blocklistPattern.toJSON() : "") + blocklistPattern: (runtime.conf.blocklistPattern ? runtime.conf.blocklistPattern : "") }, function(resp) { if (resp.state === "disabled") { if (resp.blocklist.hasOwnProperty(".*")) { @@ -350,10 +349,10 @@ function createNormal(insert) { if (runtime.conf.smartPageBoundary && ((this === document.scrollingElement) || scrollNodes.length === 1 && this === scrollNodes[0])) { if (this.scrollTop === 0 && y < 0) { - return dispatchSKEvent('topBoundaryHit'); + return dispatchSKEvent("hints", ['topBoundaryHit']); } if (this.scrollHeight - this.scrollTop <= this.clientHeight + 1 && y > 0) { - return dispatchSKEvent('bottomBoundaryHit'); + return dispatchSKEvent("hints", ['bottomBoundaryHit']); } } if (RUNTIME.repeats > 1) { @@ -365,13 +364,13 @@ function createNormal(insert) { var d = Math.max(100, 20 * Math.log(Math.abs( x || y))); elm.smoothScrollBy(x, y, d); } else { - dispatchSKEvent('scrollStarted'); + dispatchSKEvent("hints", ['scrollStarted']); elm.scrollBy({ 'behavior': 'instant', 'left': x, 'top': y, }); - dispatchSKEvent('scrollDone'); + dispatchSKEvent("hints", ['scrollDone']); } }; elm.safeScroll_ = (prop, value, increasing) => { @@ -399,7 +398,7 @@ function createNormal(insert) { if (previousTimestamp === 0) { // init previousTimestamp in first step previousTimestamp = t; - dispatchSKEvent('scrollStarted'); + dispatchSKEvent("hints", ['scrollStarted']); return window.requestAnimationFrame(step); } var old = elm[prop], delta = (t - previousTimestamp) * distance / duration; @@ -423,7 +422,7 @@ function createNormal(insert) { || stepCompleted )// distance completed ) { elm.style.scrollBehavior = ''; - dispatchSKEvent('scrollDone'); + dispatchSKEvent("hints", ['scrollDone']); } else { window.requestAnimationFrame(step); } @@ -474,7 +473,7 @@ function createNormal(insert) { } else { rc = elm.getBoundingClientRect(); } - dispatchSKEvent('highlightElement', { + dispatchSKEvent("front", ['highlightElement', { duration: 200, rect: { top: rc.top, @@ -482,7 +481,7 @@ function createNormal(insert) { width: rc.width, height: rc.height } - }); + }]); } function changeScrollTarget(silent) { scrollNodes = Mode.getScrollableElements(); @@ -579,7 +578,7 @@ function createNormal(insert) { default: break; } - dispatchSKEvent('turnOffDOMObserver'); + dispatchSKEvent("observer", ['turnOff']); }; self.refreshScrollableElements = function () { @@ -704,7 +703,7 @@ function createNormal(insert) { // hide borders var borderStyle = elm.style.borderStyle; elm.style.borderStyle = "none"; - dispatchSKEvent('toggleStatus', [false]); + dispatchSKEvent("front", ['toggleStatus', false]); var dx = 0, dy = 0, sx, sy, sw, sh, ww, wh, dh = elm.scrollHeight, dw = elm.scrollWidth; if (elm === document.scrollingElement) { @@ -742,7 +741,7 @@ function createNormal(insert) { if (lastScrollTop === elm.scrollTop) { if (lastScrollLeft === elm.scrollLeft) { // done - dispatchSKEvent('toggleStatus', [true]); + dispatchSKEvent("front", ['toggleStatus', true]); showPopup("".format(canvas.toDataURL( "image/png" ))); // restore overflow elm.style.overflowY = overflowY; @@ -930,7 +929,7 @@ function createNormal(insert) { feature_group: 9, repeatIgnore: true, code: function() { - dispatchSKEvent('openFinder'); + dispatchSKEvent("front", ['openFinder']); } }); @@ -958,7 +957,7 @@ function createNormal(insert) { // perform inline query after 1 ms // to avoid calling on selection collapse setTimeout(() => { - dispatchSKEvent('querySelectedWord'); + dispatchSKEvent("front", ['querySelectedWord']); }, 1); } } @@ -969,7 +968,7 @@ function createNormal(insert) { _disabled = createDisabled(self); _disabled.enter(0, true); } - dispatchSKEvent('turnOffDOMObserver'); + dispatchSKEvent("observer", ['turnOff']); document.removeEventListener("mouseup", _onMouseUp); }; @@ -983,7 +982,7 @@ function createNormal(insert) { self.enable(); self.onExit = function() { - dispatchSKEvent('turnOffDOMObserver'); + dispatchSKEvent("observer", ['turnOff']); _nodesHasSKScroll.forEach(function(n) { delete n.skScrollBy; delete n.smoothScrollBy; diff --git a/src/content_scripts/common/observer.js b/src/content_scripts/common/observer.js index 10467d696..ea6873389 100644 --- a/src/content_scripts/common/observer.js +++ b/src/content_scripts/common/observer.js @@ -1,5 +1,6 @@ import { getVisibleElements, + initSKFunctionListener, } from './utils.js'; import Mode from './mode'; @@ -52,18 +53,19 @@ function startScrollNodeObserver(normal) { }); DOMObserver.isConnected = false; - document.addEventListener("surfingkeys:turnOnDOMObserver", function(evt) { - if (!DOMObserver.isConnected) { - DOMObserver.observe(document, { childList: true, subtree:true }); - DOMObserver.isConnected = true; - } - }); - - document.addEventListener("surfingkeys:turnOffDOMObserver", function(evt) { - if (DOMObserver.isConnected) { - DOMObserver.disconnect(); - DOMObserver.isConnected = false; - } + initSKFunctionListener("observer", { + turnOn: () => { + if (!DOMObserver.isConnected) { + DOMObserver.observe(document, { childList: true, subtree:true }); + DOMObserver.isConnected = true; + } + }, + turnOff: () => { + if (DOMObserver.isConnected) { + DOMObserver.disconnect(); + DOMObserver.isConnected = false; + } + }, }); } diff --git a/src/content_scripts/common/runtime.js b/src/content_scripts/common/runtime.js index 0adf35554..5db31b7f4 100644 --- a/src/content_scripts/common/runtime.js +++ b/src/content_scripts/common/runtime.js @@ -1,5 +1,8 @@ -function dispatchSKEvent(type, args) { - document.dispatchEvent(new CustomEvent(`surfingkeys:${type}`, { 'detail': args })); +function dispatchSKEvent(type, args, target) { + if (target === undefined) { + target = document; + } + target.dispatchEvent(new CustomEvent(`surfingkeys:${type}`, { 'detail': args })); } /** @@ -31,7 +34,7 @@ function RUNTIME(action, args, callback) { runtime.on('onTtsEvent', callback); } } catch (e) { - dispatchSKEvent('showPopup', ['[runtime exception] ' + e]); + dispatchSKEvent("front", ['showPopup', '[runtime exception] ' + e]); } } diff --git a/src/content_scripts/common/utils.js b/src/content_scripts/common/utils.js index 6e4d5631f..0905238ab 100644 --- a/src/content_scripts/common/utils.js +++ b/src/content_scripts/common/utils.js @@ -21,7 +21,7 @@ function getBrowserName() { } function isInUIFrame() { - return document.location.href.indexOf(chrome.extension.getURL("/")) === 0; + return window !== top && document.location.href.indexOf(chrome.runtime.getURL("/")) === 0; } function timeStampString(t) { @@ -93,7 +93,7 @@ function isElementClickable(e) { * Front.showBanner(window.location.href); */ function showBanner(msg, timeout) { - dispatchSKEvent('showBanner', [msg, timeout]) + dispatchSKEvent("front", ['showBanner', msg, timeout]) } /** @@ -106,7 +106,29 @@ function showBanner(msg, timeout) { * Front.showPopup(window.location.href); */ function showPopup(msg) { - dispatchSKEvent('showPopup', [msg]) + dispatchSKEvent("front", ['showPopup', msg]) +} + +function initSKFunctionListener(name, interfaces, capture) { + const callbacks = {}; + + const opts = capture ? {capture: true} : {}; + document.addEventListener(`surfingkeys:${name}`, function(evt) { + let args = evt.detail; + const fk = args.shift(); + if (capture) { + args.push(evt.target); + } + + if (callbacks.hasOwnProperty(fk)) { + callbacks[fk](...args); + delete callbacks[fk]; + } if (interfaces.hasOwnProperty(fk)) { + interfaces[fk](...args); + } + }, opts); + + return callbacks; } function dispatchMouseEvent(element, events, shiftKey) { @@ -150,16 +172,6 @@ function toggleQuote() { } } -function LOG(level, msg) { - // To turn on all levels: chrome.storage.local.set({"logLevels": ["log", "warn", "error"]}) - chrome.storage.local.get(["logLevels"], (r) => { - const logLevels = r && r.logLevels || ["error"]; - if (["log", "warn", "error"].indexOf(level) !== -1 && logLevels.indexOf(level) !== -1) { - console[level](msg); - } - }); -} - function isEditable(element) { return element && !element.disabled && (element.localName === 'textarea' @@ -409,7 +421,7 @@ function getTextNodes(root, pattern, flag) { function getTextNodePos(node, offset, length) { var selection = document.getSelection(); selection.setBaseAndExtent(node, offset, node, length ? (offset + length) : node.data.length); - var br = selection.getRangeAt(0).getClientRects()[0]; + var br = selection.rangeCount > 0 ? selection.getRangeAt(0).getClientRects()[0] : null; var pos = { left: -1, top: -1 @@ -537,7 +549,7 @@ function initL10n(cb) { return str; }); } else { - fetch(chrome.extension.getURL("pages/l10n.json")).then(function(res) { + fetch(chrome.runtime.getURL("pages/l10n.json")).then(function(res) { return res.json(); }).then(function(l10n) { if (typeof(l10n[lang]) === "object") { @@ -599,7 +611,7 @@ function mapInMode(mode, nks, oks, new_annotation) { } mode.mappings.add(nks, meta); if (!isInUIFrame()) { - dispatchSKEvent('addMapkey', [mode.name, nks, oks]); + dispatchSKEvent("front", ['addMapkey', mode.name, nks, oks]); } } return old_map; @@ -817,21 +829,6 @@ function flashPressedLink(link, cb) { }, 100); } -function regexFromString(str, highlight) { - var rxp = null; - const flags = runtime.getCaseSensitive(str) ? "g" : "gi"; - str = str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); - if (highlight) { - rxp = new RegExp(str.replace(/\s+/, "\|"), flags); - } else { - var words = str.split(/\s+/).map(function(w) { - return `(?=.*${w})`; - }).join(''); - rxp = new RegExp(`^${words}.*$`, flags); - } - return rxp; -} - function safeDecodeURI(url) { try { return decodeURI(url); @@ -848,16 +845,6 @@ function safeDecodeURIComponent(url) { } } -function filterByTitleOrUrl(urls, query) { - if (query && query.length) { - var rxp = regexFromString(query, false); - urls = urls.filter(function(b) { - return rxp.test(b.title) || rxp.test(safeDecodeURI(b.url)); - }); - } - return urls; -} - function getCssSelectorsOfEditable() { return "input:not([type=submit]), textarea, *[contenteditable=true], *[role=textbox], select, div.ace_cursor"; } @@ -895,7 +882,7 @@ function refreshHints(hints, pressedKeys) { function attachFaviconToImgSrc(tab, imgEl) { const browserName = getBrowserName(); if (browserName === "Chrome") { - imgEl.src = `chrome://favicon/${tab.url}`; + imgEl.src = chrome.runtime.getURL(`/_favicon/?pageUrl=${tab.url}`); } else if (browserName.startsWith("Safari")) { imgEl.src = new URL(tab.url).origin + "/favicon.ico"; } else { @@ -904,7 +891,6 @@ function attachFaviconToImgSrc(tab, imgEl) { } export { - LOG, actionWithSelectionPreserved, attachFaviconToImgSrc, constructSearchURL, @@ -912,7 +898,6 @@ export { dispatchMouseEvent, dispatchSKEvent, filterAncestors, - filterByTitleOrUrl, filterInvisibleElements, filterOverlapElements, flashPressedLink, @@ -933,6 +918,7 @@ export { htmlEncode, httpRequest, initL10n, + initSKFunctionListener, insertJS, isEditable, isElementClickable, @@ -944,7 +930,6 @@ export { mapInMode, parseAnnotation, refreshHints, - regexFromString, reportIssue, safeDecodeURI, safeDecodeURIComponent, diff --git a/src/content_scripts/common/visual.js b/src/content_scripts/common/visual.js index cebd27d1e..772369690 100644 --- a/src/content_scripts/common/visual.js +++ b/src/content_scripts/common/visual.js @@ -190,7 +190,7 @@ function createVisual(clipboard, hints) { } if (matches.length) { currentOccurrence = matches.length - 1; - dispatchSKEvent('showStatus', [[undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); } } }); @@ -204,7 +204,7 @@ function createVisual(clipboard, hints) { document.scrollingElement.scrollTop = 0; currentOccurrence = 0; if (matches.length) { - dispatchSKEvent('showStatus', [[undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); } if (getBrowserName() !== "Firefox") { @@ -456,7 +456,7 @@ function createVisual(clipboard, hints) { self.hideCursor = function () { if (document.body.contains(cursor)) { cursor.remove(); - dispatchSKEvent('cursorHidden'); + dispatchSKEvent("front", ['hideBubble']); } }; @@ -576,7 +576,7 @@ function createVisual(clipboard, hints) { break; } } - dispatchSKEvent('showStatus', [[undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); } } @@ -590,25 +590,19 @@ function createVisual(clipboard, hints) { registeredScrollNodes = []; setSanitizedContent(markHolder_, ""); markHolder_.remove(); - dispatchSKEvent('showStatus', [[undefined, undefined, ""]]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, ""]]); }; self.emptySelection = function() { document.getSelection().empty(); }; - function onCursorHiden() { - dispatchSKEvent('hideBubble'); - } - self.onEnter = function() { - document.addEventListener('surfingkeys:cursorHidden', onCursorHiden); _incState(); }; self.onExit = function() { self.visualClear(); - document.removeEventListener('surfingkeys:cursorHidden', onCursorHiden); }; function _onStateChange() { @@ -683,7 +677,7 @@ function createVisual(clipboard, hints) { } currentOccurrence = (backward ? (matches.length + currentOccurrence - 1) : (currentOccurrence + 1)) % matches.length; select(matches[currentOccurrence]); - dispatchSKEvent('showStatus', [[undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, currentOccurrence + 1 + ' / ' + matches.length]]); } else if (runtime.conf.lastQuery) { highlight(new RegExp(runtime.conf.lastQuery, "g" + (runtime.getCaseSensitive(runtime.conf.lastQuery) ? "" : "i"))); self.visualEnter(runtime.conf.lastQuery); @@ -761,7 +755,7 @@ function createVisual(clipboard, hints) { self.enter(); select(matches[currentOccurrence]); } else { - dispatchSKEvent('showStatus', [[undefined, undefined, "Pattern not found: {0}".format(query)], 1000]); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, "Pattern not found: {0}".format(query)], 1000]); } Mode.getScrollableElements().forEach(function(n) { if (n !== document.scrollingElement) { diff --git a/src/content_scripts/content.js b/src/content_scripts/content.js index da7f4af85..f427b8fec 100644 --- a/src/content_scripts/content.js +++ b/src/content_scripts/content.js @@ -10,7 +10,6 @@ import { generateQuickGuid, getRealEdit, isInUIFrame, - showPopup, createElementWithContent, getBrowserName, @@ -26,20 +25,6 @@ import createDefaultMappings from './common/default.js'; import KeyboardUtils from './common/keyboardUtils'; -/* - * run user snippets, and return settings updated in snippets - */ -function runScript(api, snippets) { - var result = { settings: {}, error: "" }; - try { - var F = new Function('settings', 'api', snippets); - F(result.settings, api); - } catch (e) { - result.error = e.toString(); - } - return result; -} - /* * Apply custom key mappings for basic users, the input is like * {"a": "b", "b": "a", "c": "d"} @@ -68,74 +53,25 @@ function applyBasicMappings(api, normal, mappings) { } } -function isEmptyObject(obj) { - for (var name in obj) { - return false; +function ensureRegex(regexName) { + const r = runtime.conf[regexName] + if (r && r.source && !(r instanceof RegExp)) { + runtime.conf[regexName] = new RegExp(r.source, r.flags); } - return true; } -function applySettings(api, normal, rs) { - for (var k in rs) { - if (runtime.conf.hasOwnProperty(k)) { - runtime.conf[k] = rs[k]; - } - } - if ('findHistory' in rs) { - runtime.conf.lastQuery = rs.findHistory.length ? rs.findHistory[0] : ""; - } - if (!rs.showAdvanced) { - if (rs.basicMappings) { - applyBasicMappings(api, normal, rs.basicMappings); - } - if (rs.disabledSearchAliases) { - for (const key in rs.disabledSearchAliases) { - api.removeSearchAlias(key); - } - } - } - if (rs.showAdvanced && 'snippets' in rs && rs.snippets && !isInUIFrame()) { - var delta = runScript(api, rs.snippets); - if (delta.error !== "") { - if (window === top) { - showPopup("[SurfingKeys] Error found in settings: " + delta.error); - } else { - console.log("[SurfingKeys] Error found in settings({0}): {1}".format(window.location.href, delta.error)); - } - } - if (!isEmptyObject(delta.settings)) { - dispatchSKEvent('setUserSettings', JSON.parse(JSON.stringify(delta.settings))); - // overrides local settings from snippets - for (var k in delta.settings) { - if (runtime.conf.hasOwnProperty(k)) { - runtime.conf[k] = delta.settings[k]; - delete delta.settings[k]; - } - } - if (Object.keys(delta.settings).length > 0 && window === top) { - // left settings are for background, need not broadcast the update, neither persist into storage - RUNTIME('updateSettings', { - scope: "snippets", - settings: delta.settings - }); - } - } - } - if (runtime.conf.showProxyInStatusBar && 'proxyMode' in rs) { - var proxyMode = rs.proxyMode; - if (["byhost", "always"].indexOf(rs.proxyMode) !== -1) { - proxyMode = "{0}: {1}".format(rs.proxyMode, rs.proxy); - } - dispatchSKEvent('showStatus', [[undefined, undefined, undefined, proxyMode]]); - } - +function applyRuntimeConf(normal) { + ensureRegex("prevLinkRegex"); + ensureRegex("nextLinkRegex"); + ensureRegex("clickablePat"); RUNTIME('getState', { - blocklistPattern: runtime.conf.blocklistPattern ? runtime.conf.blocklistPattern.toJSON() : undefined, - lurkingPattern: runtime.conf.lurkingPattern ? runtime.conf.lurkingPattern.toJSON() : undefined + blocklistPattern: runtime.conf.blocklistPattern ? runtime.conf.blocklistPattern : undefined, + lurkingPattern: runtime.conf.lurkingPattern ? runtime.conf.lurkingPattern : undefined }, function (resp) { let state = resp.state; if (state === "disabled") { normal.disable(); + dispatchSKEvent("front", ['showStatus', [undefined, undefined, undefined, ""]]); } else if (state === "lurking") { state = normal.startLurk(); } else { @@ -151,10 +87,54 @@ function applySettings(api, normal, rs) { RUNTIME('setSurfingkeysIcon', { status: state }); + var proxyMode = ""; + if (state === "enabled" && runtime.conf.showProxyInStatusBar && resp.proxyMode) { + proxyMode = resp.proxyMode; + if (["byhost", "always"].indexOf(resp.proxyMode) !== -1) { + proxyMode = "{0}: {1}".format(resp.proxyMode, resp.proxy); + } + } + dispatchSKEvent("front", ['showStatus', [undefined, undefined, undefined, proxyMode]]); } }); } +function applySettings(api, normal, rs) { + for (var k in rs) { + if (runtime.conf.hasOwnProperty(k)) { + runtime.conf[k] = rs[k]; + } + } + if ('findHistory' in rs) { + runtime.conf.lastQuery = rs.findHistory.length ? rs.findHistory[0] : ""; + } + if (!rs.showAdvanced) { + if (rs.basicMappings) { + applyBasicMappings(api, normal, rs.basicMappings); + } + if (rs.disabledSearchAliases) { + for (const key in rs.disabledSearchAliases) { + api.removeSearchAlias(key); + } + } + } else if (!rs.isMV3) { + import(/* webpackIgnore: true */ chrome.runtime.getURL("/api.js")).then((module) => { + module.default(chrome.runtime.getURL("/"), (api, settings) => { + try { + (new Function('settings', 'api', rs.snippets))(settings, api); + } catch (e) { + showBanner(e.toString(), 3000); + } + }); + }); + } + + applyRuntimeConf(normal); + document.addEventListener("surfingkeys:settingsFromSnippetsLoaded", () => { + applyRuntimeConf(normal); + }, {once: true}); +} + function _initModules() { const clipboard = createClipboard(); const insert = createInsert(); @@ -166,7 +146,7 @@ function _initModules() { const front = createFront(insert, normal, hints, visual, _browser); const api = createAPI(clipboard, insert, normal, hints, visual, front, _browser); - createDefaultMappings(api); + createDefaultMappings(api, clipboard, insert, normal, hints, visual, front); if (typeof(_browser.plugin) === "function") { _browser.plugin({ front }); } @@ -175,7 +155,10 @@ function _initModules() { RUNTIME('getSettings', null, function(response) { var rs = response.settings; applySettings(api, normal, rs); - dispatchSKEvent('userSettingsLoaded', {settings: rs, api, front}); + const disabledSearchAliases = rs.disabledSearchAliases; + const getUsage = front.getUsage; + const frontCommand = front.command; + dispatchSKEvent('userSettingsLoaded', {settings: rs, disabledSearchAliases, getUsage, frontCommand}); }); return { normal, @@ -224,7 +207,7 @@ function start(browser) { }; if (window === top) { new Promise((r, j) => { - if (window.location.href === chrome.extension.getURL("/pages/options.html")) { + if (window.location.href === chrome.runtime.getURL("/pages/options.html")) { import(/* webpackIgnore: true */ './pages/options.js').then((optionsLib) => { optionsLib.default( RUNTIME, @@ -258,13 +241,18 @@ function start(browser) { runtime.on('tabDeactivated', function() { modes.front.detach(); }); + runtime.on('setScrollPos', function(msg, sender, response) { + setTimeout(() => { + document.scrollingElement.scrollLeft = msg.scrollLeft; + document.scrollingElement.scrollTop = msg.scrollTop; + }, 1000); + }); + runtime.on('showBanner', function(msg, sender, response) { + showBanner(msg.message, 3000); + }); document.addEventListener("surfingkeys:ensureFrontEnd", function(evt) { modes.front.attach(); }); - window._setScrollPos = function (x, y) { - document.scrollingElement.scrollLeft = x; - document.scrollingElement.scrollTop = y; - }; RUNTIME('tabURLAccessed', { title: document.title, diff --git a/src/content_scripts/front.js b/src/content_scripts/front.js index 060fbdca4..77236ce96 100644 --- a/src/content_scripts/front.js +++ b/src/content_scripts/front.js @@ -1,3 +1,4 @@ +import Mode from './common/mode.js'; import { createElementWithContent, flashPressedLink, @@ -5,9 +6,12 @@ import { getAnnotations, getBrowserName, getDocumentOrigin, + getElements, httpRequest, + initSKFunctionListener, isEditable, isInUIFrame, + scrollIntoViewIfNeeded, tabOpenLink, } from './common/utils.js'; import { RUNTIME, dispatchSKEvent, runtime } from './common/runtime.js'; @@ -22,10 +26,7 @@ function createFront(insert, normal, hints, visual, browser) { var _uiUserSettings = []; function applyUserSettings() { for (var cmd of _uiUserSettings) { - cmd.toFrontend = true; - cmd.origin = getDocumentOrigin(); - cmd.id = generateQuickGuid(); - runtime.postTopMessage(cmd); + self.command(cmd); } } @@ -65,19 +66,21 @@ function createFront(insert, normal, hints, visual, browser) { } }; - document.addEventListener("surfingkeys:setUserSettings", function(evt) { - _uiUserSettings.push({ - action: 'applyUserSettings', - userSettings: evt.detail - }); - }); + function applyUICommand(cmd) { + _uiUserSettings.push(cmd); + if (frontendPromise) { + frontendPromise.then(function() { + self.command(cmd); + }); + } + } var _listSuggestions = {}; self.addSearchAlias = function (alias, prompt, url, suggestionURL, listSuggestion, options) { if (suggestionURL && listSuggestion) { _listSuggestions[suggestionURL] = listSuggestion; } - _uiUserSettings.push({ + applyUICommand({ action: 'addSearchAlias', alias: alias, prompt: prompt, @@ -87,19 +90,20 @@ function createFront(insert, normal, hints, visual, browser) { }); }; self.removeSearchAlias = function (alias) { - _uiUserSettings.push({ + applyUICommand({ action: 'removeSearchAlias', alias: alias }); }; var _actions = {}; + var skCallbacks = {}; self.performInlineQueryOnSelection = function(word) { var b = document.getSelection().getRangeAt(0).getClientRects()[0]; self.performInlineQuery(word, b, function(pos, queryResult) { if (queryResult) { - dispatchSKEvent('showBubble', [{ + dispatchSKEvent("front", ['showBubble', { top: pos.top, left: pos.left, height: pos.height, @@ -115,7 +119,6 @@ function createFront(insert, normal, hints, visual, browser) { self.performInlineQueryOnSelection(word); } } - document.addEventListener("surfingkeys:querySelectedWord", querySelectedWord); _actions["updateInlineQuery"] = function (message) { if (message.word) { @@ -128,10 +131,25 @@ function createFront(insert, normal, hints, visual, browser) { _actions["getSearchSuggestions"] = function (message) { var ret = null; if (_listSuggestions.hasOwnProperty(message.url)) { - ret = _listSuggestions[message.url](message.response, { - url: message.requestUrl, - query: message.query, - }); + const listSuggestion = _listSuggestions[message.url]; + if (typeof listSuggestion === "function") { + ret = listSuggestion(message.response, { + url: message.requestUrl, + query: message.query, + }); + } else { + ret = new Promise((resolve, reject) => { + const callbackId = generateQuickGuid(); + skCallbacks[callbackId] = (res) => { + resolve(res); + }; + + dispatchSKEvent('user', ["getSearchSuggestions", message.url, message.response, { + url: message.requestUrl, + query: message.query, + }, callbackId]); + }); + } } return ret; }; @@ -142,30 +160,6 @@ function createFront(insert, normal, hints, visual, browser) { cmdline: cmd }); }; - document.addEventListener("surfingkeys:addMapkey", function(evt) { - const [ mode, new_keystroke, old_keystroke ] = evt.detail; - _uiUserSettings.push({ - action: 'addMapkey', - mode: mode, - new_keystroke: new_keystroke, - old_keystroke: old_keystroke - }); - }); - document.addEventListener("surfingkeys:addVimMap", function(evt) { - const [ lhs, rhs, ctx ] = evt.detail; - _uiUserSettings.push({ - action: 'addVimMap', - lhs: lhs, - rhs: rhs, - ctx: ctx - }); - }); - document.addEventListener("surfingkeys:addVimKeyMap", function(evt) { - _uiUserSettings.push({ - action: 'addVimKeyMap', - vimKeyMap: evt.detail - }); - }); var frameElement = createElementWithContent('div', 'Hi, I\'m here now!', {id: "sk_frame"}); frameElement.fromSurfingKeys = true; @@ -181,9 +175,6 @@ function createFront(insert, normal, hints, visual, browser) { frameElement.remove(); }, sn.duration); } - document.addEventListener("surfingkeys:highlightElement", function(evt) { - highlightElement(evt.detail); - }); function getAllAnnotations() { let mappings = [ normal.mappings, @@ -215,20 +206,11 @@ function createFront(insert, normal, hints, visual, browser) { }); }; - document.addEventListener("surfingkeys:showPopup", function(evt) { - const [ content ] = evt.detail; - self.command({ - action: 'showPopup', - content: content - }); - }); - function hidePopup() { self.command({ action: 'hidePopup' }); } - document.addEventListener("surfingkeys:hidePopup", hidePopup); function updateElementBehindEditor(data) { // setEditorText and setValueWithEventDispatched are experimental APIs from Brook Build of Chromium @@ -363,40 +345,38 @@ function createFront(insert, normal, hints, visual, browser) { self.command(args); }; - var _inlineQuery; + var _inlineQuery = false; var _showQueryResult; self.performInlineQuery = function (query, pos, showQueryResult) { - if (document.dictEnabled !== undefined) { - if (document.dictEnabled) { - if (window.location.protocol === "dictorium:") { - if (window === top) { - window.location.href = query; - } else { - window.postMessage({dictorium_data: { type: 'DictoriumReload', word: query }}); - } + if (document.dictEnabled !== undefined && document.dictEnabled) { + if (window.location.protocol === "dictorium:") { + if (window === top) { + window.location.href = query; } else { - window.postMessage({dictorium_data: { - type: "OpenDictoriumQuery", - word: query, - sentence: "", - pos: pos, - source: window.location.href - }}); + window.postMessage({dictorium_data: { type: 'DictoriumReload', word: query }}); } - hidePopup(); + } else { + window.postMessage({dictorium_data: { + type: "OpenDictoriumQuery", + word: query, + sentence: "", + pos: pos, + source: window.location.href + }}); } + hidePopup(); } else if (_inlineQuery) { if (runtime.conf.autoSpeakOnInlineQuery) { browser.readText(query); } query = query.toLocaleLowerCase(); runtime.updateHistory('OmniQuery', query); - httpRequest({ - url: (typeof(_inlineQuery.url) === "function") ? _inlineQuery.url(query) : _inlineQuery.url + query, - headers: _inlineQuery.headers - }, function(res) { - showQueryResult(pos, _inlineQuery.parseResult(res)); - }); + + const callbackId = generateQuickGuid(); + skCallbacks[callbackId] = (res) => { + showQueryResult(pos, res); + }; + dispatchSKEvent('user', ["performInlineQuery", query, callbackId]); } else if (isInUIFrame()) { _showQueryResult = function(result) { showQueryResult(pos, result); @@ -420,95 +400,19 @@ function createFront(insert, normal, hints, visual, browser) { * * @see [example](https://github.com/brookhong/Surfingkeys/wiki/Register-inline-query). */ - self.registerInlineQuery = function(args) { - _inlineQuery = args; + self.registerInlineQuery = function() { + _inlineQuery = true; }; self.openOmniquery = function(args) { self.openOmnibar(({type: "OmniQuery", extra: args.query, style: args.style})); }; - document.addEventListener("surfingkeys:openFinder", function(evt) { - self.command({ - action: "openFinder" - }); - }); - - document.addEventListener("surfingkeys:showBanner", function(evt) { - const [ msg, linger_time ] = evt.detail; - self.command({ - action: "showBanner", - content: msg, - linger_time: linger_time - }); - }); - - document.addEventListener("surfingkeys:showBubble", function(evt) { - const [ pos, msg, noPointerEvents ] = evt.detail; - if (msg.length > 0) { - pos.winWidth = window.innerWidth; - pos.winHeight = window.innerHeight; - pos.winX = 0; - pos.winY = 0; - if (window.frameElement) { - pos.winX = window.frameElement.offsetLeft; - pos.winY = window.frameElement.offsetTop; - } - self.command({ - action: "showBubble", - content: msg, - position: pos, - noPointerEvents: noPointerEvents - }); - } - }); - - document.addEventListener("surfingkeys:hideBubble", function(evt) { - self.command({ - action: 'hideBubble' - }); - }); - var _keyHints = { accumulated: "", candidates: {}, key: "" }; - document.addEventListener("surfingkeys:hideKeystroke", function(evt) { - _keyHints.accumulated = ""; - _keyHints.candidates = {}; - self.command({ - action: 'hideKeystroke' - }); - }); - - document.addEventListener("surfingkeys:showKeystroke", function(evt) { - const [ key, mode ] = evt.detail; - _keyHints.accumulated += key; - _keyHints.key = key; - _keyHints.candidates = {}; - - var root = mode.mappings.find(_keyHints.accumulated); - if (root) { - root.getMetas(function(m) { - return true; - }).forEach(function(m) { - _keyHints.candidates[m.word] = { - annotation: m.annotation - }; - }); - } - - self.command({ - action: 'showKeystroke', - keyHints: _keyHints - }); - }); - - document.addEventListener("surfingkeys:showStatus", function(evt) { - self.showStatus(...evt.detail); - }); - self.showStatus = function (msgs, duration) { self.command({ action: "showStatus", @@ -523,6 +427,130 @@ function createFront(insert, normal, hints, visual, browser) { }); }; + skCallbacks = initSKFunctionListener("front", { + showPopup: (content) => { + self.command({ + action: 'showPopup', + content: content + }); + }, + applySettingsFromSnippets: (us) => { + applyUICommand({ + action: 'applyUserSettings', + userSettings: us + }); + const cloneUS = JSON.parse(JSON.stringify(us)); + // overrides local settings from snippets + for (var k in cloneUS) { + if (runtime.conf.hasOwnProperty(k)) { + runtime.conf[k] = cloneUS[k]; + delete cloneUS[k]; + } + } + if (runtime.conf.enableEmojiInsertion) { + insert.enableEmojiInsertion(); + } + if (Object.keys(cloneUS).length > 0 && window === top) { + // left settings are for background, need not broadcast the update, neither persist into storage + RUNTIME('updateSettings', { + scope: "snippets", + settings: cloneUS + }); + } + dispatchSKEvent('settingsFromSnippetsLoaded'); + }, + querySelectedWord, + addMapkey: (mode, new_keystroke, old_keystroke) => { + applyUICommand({ + action: 'addMapkey', + mode: mode, + new_keystroke: new_keystroke, + old_keystroke: old_keystroke + }); + }, + addVimMap: (lhs, rhs, ctx) => { + applyUICommand({ + action: 'addVimMap', + lhs: lhs, + rhs: rhs, + ctx: ctx + }); + }, + addVimKeyMap: (vimKeyMap) => { + applyUICommand({ + action: 'addVimKeyMap', + vimKeyMap + }); + }, + highlightElement, + hidePopup, + openFinder: () => { + self.command({ + action: "openFinder" + }); + }, + showBanner: (msg, linger_time) => { + self.command({ + action: "showBanner", + content: msg, + linger_time: linger_time + }); + }, + showBubble: (pos, msg, noPointerEvents) => { + if (msg.length > 0) { + pos.winWidth = window.innerWidth; + pos.winHeight = window.innerHeight; + pos.winX = 0; + pos.winY = 0; + if (window.frameElement) { + pos.winX = window.frameElement.offsetLeft; + pos.winY = window.frameElement.offsetTop; + } + self.command({ + action: "showBubble", + content: msg, + position: pos, + noPointerEvents: noPointerEvents + }); + } + }, + hideBubble: () => { + self.command({ + action: 'hideBubble' + }); + }, + hideKeystroke: () => { + _keyHints.accumulated = ""; + _keyHints.candidates = {}; + self.command({ + action: 'hideKeystroke' + }); + }, + showKeystroke: (key, mode) => { + _keyHints.accumulated += key; + _keyHints.key = key; + _keyHints.candidates = {}; + + var root = mode.mappings.find(_keyHints.accumulated); + if (root) { + root.getMetas(function(m) { + return true; + }).forEach(function(m) { + _keyHints.candidates[m.word] = { + annotation: m.annotation + }; + }); + } + + self.command({ + action: 'showKeystroke', + keyHints: _keyHints + }); + }, + showStatus: self.showStatus, + toggleStatus: self.toggleStatus, + }); + _actions["ace_editor_saved"] = function(response) { if (response.data !== undefined) { onEditorSaved(response.data); @@ -572,17 +600,6 @@ function createFront(insert, normal, hints, visual, browser) { }); }; - _actions["executeScript"] = function(message) { - RUNTIME('executeScript', { - code: message.cmdline - }, function (response) { - self.command({ - action: 'updateOmnibarResult', - words: response.response - }); - }); - }; - _actions["getBackFocus"] = function(response) { window.focus(); if (window === top && frontendPromise) { @@ -684,8 +701,11 @@ function createFront(insert, normal, hints, visual, browser) { } } else if (_message.action && _actions.hasOwnProperty(_message.action)) { var ret = _actions[_message.action](_message); - if (_message.ack) { - Promise.resolve(ret).then((data) => + if (_message.ack && ret) { + if (!ret.then) { + ret = Promise.resolve(ret); + } + ret.then((data) => runtime.postTopMessage({ data, toFrontend: true, diff --git a/src/content_scripts/options.js b/src/content_scripts/options.js index 1c31373b7..8bdf0609c 100644 --- a/src/content_scripts/options.js +++ b/src/content_scripts/options.js @@ -218,19 +218,19 @@ export default function( }); } - var basicSettingsDiv = document.getElementById("basicSettings"); - var basicMappingsDiv = document.getElementById("basicMappings"); - var advancedSettingDiv = document.getElementById("advancedSetting"); - var advancedTogglerDiv = document.getElementById("advancedToggler"); + const basicSettingsDiv = document.getElementById("basicSettings"); + const basicMappingsDiv = document.getElementById("basicMappings"); + const advancedSettingDiv = document.getElementById("advancedSetting"); + const advancedToggler = document.getElementById("advancedToggler"); function showAdvanced(flag) { if (flag) { basicSettingsDiv.hide(); advancedSettingDiv.show(); - advancedTogglerDiv.setAttribute('checked', 'checked'); + advancedToggler.setAttribute('checked', 'checked'); } else { basicSettingsDiv.show(); advancedSettingDiv.hide(); - advancedTogglerDiv.removeAttribute('checked'); + advancedToggler.removeAttribute('checked'); } } @@ -238,7 +238,13 @@ export default function( var localPathInput = document.getElementById("localPath"); var sample = document.getElementById("sample").innerHTML; function renderSettings(rs) { - showAdvanced(rs.showAdvanced); + if (rs.isMV3) { + document.getElementById("advancedTip").innerText = "If you're an advanced user, please turn on Developer mode from chrome://extensions/ then the flag here."; + advancedToggler.disabled = !rs.isUserScriptsAvailable; + showAdvanced(rs.isUserScriptsAvailable && rs.showAdvanced); + } else { + showAdvanced(rs.showAdvanced); + } if (rs.localPath) { localPathInput.value = rs.localPath; localPathSaved = rs.localPath; @@ -254,21 +260,19 @@ export default function( renderProxySettings(rs); } - RUNTIME('getSettings', null, function(response) { - mappingsEditor = createMappingEditor('mappings'); - renderSettings(response.settings); - if ('error' in response.settings) { - showBanner(response.settings.error, 5000); - } - }); - advancedTogglerDiv.onclick = function() { + advancedToggler.onclick = function() { var newFlag = this.checked; - showAdvanced(newFlag); RUNTIME('updateSettings', { settings: { showAdvanced: newFlag } + }, (resp) => { + if (resp.error) { + showBanner(resp.error, 3000); + } else { + showAdvanced(newFlag); + } }); }; document.getElementById('resetSettings').onclick = function() { @@ -349,10 +353,10 @@ export default function( }).filter((m) => m !== null);; }); - function renderSearchAlias(front, disabledSearchAliases) { + function renderSearchAlias(frontCommand, disabledSearchAliases) { new Promise((r, j) => { const getSearchAliases = () => { - front.command({ + frontCommand({ action: 'getSearchAliases' }, function(response) { if (Object.keys(response.aliases).length > 0) { @@ -420,8 +424,13 @@ export default function( } document.addEventListener("surfingkeys:userSettingsLoaded", function(evt) { - const { settings, front } = evt.detail; - renderSearchAlias(front, settings.disabledSearchAliases || {}); + const { settings, disabledSearchAliases, frontCommand } = evt.detail; + mappingsEditor = createMappingEditor('mappings'); + renderSettings(settings); + if ('error' in settings) { + showBanner(settings.error, 5000); + } + renderSearchAlias(frontCommand, disabledSearchAliases || {}); renderKeyMappings(settings); }); diff --git a/src/content_scripts/start.js b/src/content_scripts/start.js index 5e5a6bebb..ba22940a8 100644 --- a/src/content_scripts/start.js +++ b/src/content_scripts/start.js @@ -6,7 +6,8 @@ import { marked } from 'marked'; RUNTIME("getTopSites", null, function(response) { var urls = response.urls.map(function(u) { - return `
  • ${u.title}
  • `; + const favUrl = chrome.runtime.getURL(`/_favicon/?pageUrl=${u.url}`); + return `
  • ${u.title}
  • `; }); setSanitizedContent(document.querySelector("#topSites>ul"), urls.join("\n")); var source = document.getElementById('quickIntroSource').innerHTML; @@ -44,8 +45,8 @@ RUNTIME("getTopSites", null, function(response) { }); document.addEventListener("surfingkeys:userSettingsLoaded", function(evt) { - const { api } = evt.detail; - api.Front.getUsage(function(usage) { + const { getUsage } = evt.detail; + getUsage(function(usage) { var _usage = document.getElementById('sk_usage'); setSanitizedContent(_usage, usage); var keys = Array.from(_usage.querySelectorAll('div')).filter(function(d) { diff --git a/src/content_scripts/ui/command.js b/src/content_scripts/ui/command.js index 92025f0d6..48c88c82f 100644 --- a/src/content_scripts/ui/command.js +++ b/src/content_scripts/ui/command.js @@ -123,10 +123,4 @@ export default (normal, command, omnibar) => { var dt = new Date(parseInt(args[0])); omnibar.listWords([dt.toString()]); }); - command('userAgent', 'set user agent', function(args) { - // 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1' - RUNTIME('setUserAgent', { - userAgent: args.join(' ') - }); - }); } diff --git a/src/content_scripts/ui/frontend.js b/src/content_scripts/ui/frontend.js index 74458cff2..9bf37b12a 100644 --- a/src/content_scripts/ui/frontend.js +++ b/src/content_scripts/ui/frontend.js @@ -8,6 +8,7 @@ import { getWordUnderCursor, htmlEncode, initL10n, + initSKFunctionListener, refreshHints, setSanitizedContent, mapInMode @@ -21,6 +22,7 @@ import createNormal from '../common/normal.js'; import createVisual from '../common/visual.js'; import createHints from '../common/hints.js'; import createAPI from '../common/api.js'; +import createDefaultMappings from '../common/default.js'; import createOmnibar from './omnibar.js'; import createCommands from './command.js'; @@ -48,6 +50,7 @@ const Front = (function() { }; const api = createAPI(clipboard, insert, normal, hints, visual, self, {}); + createDefaultMappings(api, clipboard, insert, normal, hints, visual, self); var _actions = self._actions, _callbacks = {}; @@ -220,7 +223,6 @@ const Front = (function() { } else { clearInterval(inputGuard); } - console.log(inputGuard); }, 100); } }; @@ -296,6 +298,7 @@ const Front = (function() { } }); }; + self.chooseTab = _actions['chooseTab']; function buildUsage(metas, cb) { var feature_groups = [ @@ -396,10 +399,6 @@ const Front = (function() { showPopup(message.content); }; - document.addEventListener("surfingkeys:showPopup", function(evt) { - showPopup(...evt.detail); - }); - self.vimMappings = []; let _aceEditor = null; _editor.onShow = function(message) { @@ -476,9 +475,6 @@ const Front = (function() { _actions['openFinder'] = function() { Find.open(); }; - document.addEventListener("surfingkeys:openFinder", function(evt) { - Find.open(); - }); function showBanner(content, linger_time) { _banner.style.cssText = ""; @@ -497,9 +493,6 @@ const Front = (function() { _actions['showBanner'] = function(message) { showBanner(message.content, message.linger_time); }; - document.addEventListener("surfingkeys:showBanner", function(evt) { - showBanner(...evt.detail); - }); _actions['showBubble'] = function(message) { var pos = message.position; pos.left += pos.winX; @@ -563,8 +556,15 @@ const Front = (function() { StatusBar.show(message.contents, message.duration); }; - document.addEventListener("surfingkeys:showStatus", function(evt) { - StatusBar.show(...evt.detail); + initSKFunctionListener("front", { + showPopup, + showBanner, + openFinder: () => { + Find.open(); + }, + showStatus: (contents, duration) => { + StatusBar.show(contents, duration); + }, }); self.toggleStatus = function(visible) { @@ -1071,7 +1071,7 @@ function createAceEditor(normal, front) { }; vim.defineEx("wq", "wq", wq); vim.defineEx("x", "x", wq); - vim.map('', ':wq', 'normal'); + vim.map('', ':wq', 'normal'); vim.defineEx("bnext", "bn", function(cm, input) { front.contentCommand({ action: 'nextEdit', @@ -1159,17 +1159,17 @@ function createAceEditor(normal, front) { vim.unmap('', 'insert'); vim.unmap('', 'insert'); if (message.type === 'url') { - vim.map('', ':wq', 'insert'); + vim.map('', ':wq', 'insert'); _ace.setOption('showLineNumbers', false); _ace.language_tools.setCompleters([createUrlCompleter()]); _ace.container.style.height = "30%"; } else if (message.type === 'input') { - vim.map('', ':wq', 'insert'); + vim.map('', ':wq', 'insert'); _ace.setOption('showLineNumbers', false); _ace.language_tools.setCompleters([pageWordCompleter]); _ace.container.style.height = "16px"; } else { - vim.map('', ':wq', 'insert'); + vim.map('', ':wq', 'insert'); _ace.setOption('showLineNumbers', true); _ace.language_tools.setCompleters([pageWordCompleter]); _ace.container.style.height = "30%"; diff --git a/src/content_scripts/ui/omnibar.js b/src/content_scripts/ui/omnibar.js index fc3d69278..01ba0f1a4 100644 --- a/src/content_scripts/ui/omnibar.js +++ b/src/content_scripts/ui/omnibar.js @@ -2,15 +2,17 @@ import Trie from '../common/trie'; import KeyboardUtils from '../common/keyboardUtils'; import Mode from '../common/mode'; import { debounce } from 'lodash'; +import { + filterByTitleOrUrl, + regexFromString, +} from '../../common/utils.js'; import { attachFaviconToImgSrc, constructSearchURL, createElementWithContent, - filterByTitleOrUrl, getBrowserName, htmlEncode, parseAnnotation, - regexFromString, safeDecodeURI, safeDecodeURIComponent, scrollIntoViewIfNeeded, @@ -488,7 +490,7 @@ function createOmnibar(front, clipboard) { var query = self.input.value.trim(); var rxp = null; if (query.length) { - rxp = regexFromString(query, true); + rxp = regexFromString(query, runtime.getCaseSensitive(query), true); } self.listResults(_page, function(b) { var li; @@ -686,7 +688,7 @@ function createOmnibar(front, clipboard) { var results = response.tabs; RUNTIME("getTopSites", null, function(response) { results = results.concat(response.urls); - results = filterByTitleOrUrl(results, self.input.value); + results = filterByTitleOrUrl(results, self.input.value, runtime.getCaseSensitive(self.input.value)); self.listBookmarkFolders(function() { RUNTIME('getAllURLs', { maxResults: self.getHistoryCacheSize() - results.length, @@ -703,14 +705,14 @@ function createOmnibar(front, clipboard) { self.addHandler('RecentlyClosed', OpenURLs(`Recently closed${separatorHtml}`, self, () => { return new Promise((resolve, reject) => { RUNTIME('getRecentlyClosed', null, function(response) { - resolve(filterByTitleOrUrl(response.urls, self.input.value)); + resolve(filterByTitleOrUrl(response.urls, self.input.value, runtime.getCaseSensitive(self.input.value))); }); }); })); self.addHandler('TabURLs', OpenURLs(`Tab History${separatorHtml}`, self, () => { return new Promise((resolve, reject) => { RUNTIME('getTabURLs', null, function(response) { - resolve(filterByTitleOrUrl(response.urls, self.input.value)); + resolve(filterByTitleOrUrl(response.urls, self.input.value, runtime.getCaseSensitive(self.input.value))); }); }); })); @@ -1060,7 +1062,7 @@ function OpenTabs(omnibar) { }; self.onInput = function() { omnibar.cachedPromise.then(function(cached) { - var filtered = filterByTitleOrUrl(cached, omnibar.input.value); + var filtered = filterByTitleOrUrl(cached, omnibar.input.value, runtime.getCaseSensitive(omnibar.input.value)); omnibar.listURLs(filtered, false); }); }; @@ -1105,7 +1107,7 @@ function OpenWindows(omnibar, front) { const query = omnibar.input.value; let rxp = null; if (query && query.length) { - rxp = regexFromString(query, false); + rxp = regexFromString(query, runtime.getCaseSensitive(query), false); filtered = cached.filter(function(w) { for (const t of w.tabs) { if (rxp.test(t.title) || rxp.test(t.url)) { @@ -1115,7 +1117,7 @@ function OpenWindows(omnibar, front) { return false; }); } - rxp = regexFromString(query, true); + rxp = regexFromString(query, runtime.getCaseSensitive(query), true); omnibar.listResults(filtered, function(w) { const li = createElementWithContent('li'); li.windowId = parseInt(w.id); @@ -1229,7 +1231,8 @@ function SearchEngine(omnibar, front) { }; function listSuggestions(suggestions) { omnibar.detectAndInsertURLItem(omnibar.input.value, suggestions); - var rxp = regexFromString(encodeURIComponent(omnibar.input.value), true); + const query = encodeURIComponent(omnibar.input.value); + var rxp = regexFromString(query, runtime.getCaseSensitive(query), true); omnibar.listResults(suggestions, function (w) { if (w.hasOwnProperty('html')) { return omnibar.createItemFromRawHtml(w); @@ -1405,10 +1408,7 @@ function Commands(omnibar, front) { var meta = items[cmd]; meta.code.call(meta.code, args); } else { - front.contentCommand({ - action: 'executeScript', - cmdline: cmdline - }); + showBanner(`Unsupported command: ${cmdline}.`, 3000); } } @@ -1496,7 +1496,7 @@ function OpenUserURLs(omnibar) { var query = omnibar.input.value; var urls = []; - urls = filterByTitleOrUrl(_items, query); + urls = filterByTitleOrUrl(_items, query, runtime.getCaseSensitive(query)); omnibar.listURLs(urls, false); }; return self; diff --git a/src/manifest.json b/src/manifest.json index 31e91a6f9..6ca8bdbbb 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -36,7 +36,6 @@ "author": "brook hong", "permissions": [ "nativeMessaging", - "", "tabs", "history", "bookmarks", diff --git a/src/pages/options.html b/src/pages/options.html index 722bc2b55..503315211 100644 --- a/src/pages/options.html +++ b/src/pages/options.html @@ -82,8 +82,11 @@

    For below hosts, above proxy will be used, click ❌ to remove one.

    Pick up a new key, Backsapce to del backward, Enter to confirm, Esc to cancel.

     
    - Advanced mode - If you're an advanced user, turn on it here. +
    +

    Support Surfingkeys with paypal.

    +
    + Advanced mode + If you're an advanced user, turn on it here.

    Search Engine aliases

    @@ -136,10 +139,6 @@

    Proxy Settings

    Reset

    -
    -

    Thank you for donation! Support me with paypal, or

    - -
    diff --git a/src/user_scripts/index.js b/src/user_scripts/index.js new file mode 100644 index 000000000..dc34f832f --- /dev/null +++ b/src/user_scripts/index.js @@ -0,0 +1,288 @@ +import { RUNTIME, dispatchSKEvent } from '../content_scripts/common/runtime.js'; +import { + httpRequest, + getBrowserName, + getClickableElements, + initSKFunctionListener, + isElementPartiallyInViewport, + showBanner, + showPopup, + tabOpenLink, +} from '../content_scripts/common/utils.js'; + +var EXTENSION_ROOT_URL = ""; +function isInUIFrame() { + return !document.location.href.startsWith("chrome://") && document.location.href.indexOf(EXTENSION_ROOT_URL) === 0; +} + + function _isDomainApplicable(domain) { + return !domain || domain.test(document.location.href) || domain.test(window.origin); + } + + function cmap(new_keystroke, old_keystroke, domain, new_annotation) { + if (_isDomainApplicable(domain)) { + dispatchSKEvent("front", ['addMapkey', "Omnibar", new_keystroke, old_keystroke]); + } + } + + /** + * Map the key sequence `lhs` to `rhs` for mode `ctx` in ACE editor. + * + * @param {string} lhs a key sequence to replace + * @param {string} rhs a key sequence to be replaced + * @param {string} ctx a mode such as `insert`, `normal`. + * + * @example aceVimMap('J', ':bn', 'normal'); + */ + function aceVimMap(lhs, rhs, ctx) { + dispatchSKEvent("front", ['addVimMap', lhs, rhs, ctx]); + } + + /** + * Add map key in ACE editor. + * + * @param {object} objects multiple objects to define key map in ACE, see more from [ace/keyboard/vim.js](https://github.com/ajaxorg/ace/blob/ec450c03b51aba3724cf90bb133708078d1f3de6/lib/ace/keyboard/vim.js#L927-L1099) + * + * @example + * addVimMapKey( + * { + * keys: 'n', + * type: 'motion', + * motion: 'moveByCharacters', + * motionArgs: { + * forward: false + * } + * }, + * + * { + * keys: 'e', + * type: 'motion', + * motion: 'moveByLines', + * motionArgs: { + * forward: true, + * linewise: true + * } + * } + * ); + */ + function addVimMapKey() { + dispatchSKEvent("front", ['addVimKeyMap', Array.from(arguments)]); + } + +const userDefinedFunctions = {}; +function mapkey(keys, annotation, jscode, options) { + if (!options || _isDomainApplicable(options.domain)) { + userDefinedFunctions[`normal:${keys}`] = jscode; + dispatchSKEvent('api', ['mapkey', keys, annotation, options]); + } +} +function imapkey(keys, annotation, jscode, options) { + if (!options || _isDomainApplicable(options.domain)) { + userDefinedFunctions[`insert:${keys}`] = jscode; + dispatchSKEvent('api', ['imapkey', keys, annotation, options]); + } +} +function vmapkey(keys, annotation, jscode, options) { + if (!options || _isDomainApplicable(options.domain)) { + userDefinedFunctions[`visual:${keys}`] = jscode; + dispatchSKEvent('api', ['vmapkey', keys, annotation, options]); + } +} + +function map(new_keystroke, old_keystroke, domain, new_annotation) { + dispatchSKEvent('api', ['map', new_keystroke, old_keystroke, domain, new_annotation]); +} +function imap(new_keystroke, old_keystroke, domain, new_annotation) { + dispatchSKEvent('api', ['imap', new_keystroke, old_keystroke, domain, new_annotation]); +} +function lmap(new_keystroke, old_keystroke, domain, new_annotation) { + dispatchSKEvent('api', ['lmap', new_keystroke, old_keystroke, domain, new_annotation]); +} +function vmap(new_keystroke, old_keystroke, domain, new_annotation) { + dispatchSKEvent('api', ['vmap', new_keystroke, old_keystroke, domain, new_annotation]); +} + +const functionsToListSuggestions = {}; + +let inlineQuery; +let hintsFunction; +let onClipboardReadFn; +initSKFunctionListener("user", { + callUserFunction: (keys) => { + if (userDefinedFunctions.hasOwnProperty(keys)) { + userDefinedFunctions[keys](); + } + }, + getSearchSuggestions: (url, response, request, callbackId, origin) => { + if (functionsToListSuggestions.hasOwnProperty(url)) { + const ret = functionsToListSuggestions[url](response, request); + dispatchSKEvent("front", [callbackId, ret]); + } + }, + performInlineQuery: (query, callbackId, origin) => { + httpRequest({ + url: (typeof(inlineQuery.url) === "function") ? inlineQuery.url(query) : inlineQuery.url + query, + headers: inlineQuery.headers + }, function(res) { + dispatchSKEvent("front", [callbackId, inlineQuery.parseResult(res)]); + }); + }, + onClipboardRead: (resp) => { + onClipboardReadFn(resp); + }, + onHintClicked: (element) => { + if (typeof(hintsFunction) === 'function') { + hintsFunction(element); + } + }, +}, true); + +function addSearchAlias(alias, prompt, search_url, search_leader_key, suggestion_url, callback_to_parse_suggestion, only_this_site_key, options) { + functionsToListSuggestions[suggestion_url] = callback_to_parse_suggestion; + dispatchSKEvent('api', ['addSearchAlias', alias, prompt, search_url, search_leader_key, suggestion_url, "user", only_this_site_key, options]); +} + +function isEmptyObject(obj) { + for (var name in obj) { + return false; + } + return true; +} + +function applyUserSettings(delta) { + if (delta.error !== "") { + if (window === top) { + showPopup("[SurfingKeys] Error found in settings: " + delta.error); + } else { + console.log("[SurfingKeys] Error found in settings({0}): {1}".format(window.location.href, delta.error)); + } + } + if (!isEmptyObject(delta.settings)) { + dispatchSKEvent("front", ['applySettingsFromSnippets', delta.settings]); + } +} + +const api = { + RUNTIME, + aceVimMap, + addVimMapKey, + addSearchAlias, + cmap, + imap, + imapkey, + isElementPartiallyInViewport, + getBrowserName, + getClickableElements, + lmap, + vmap, + vmapkey, + map, + mapkey, + unmap: (keystroke, domain) => { + dispatchSKEvent('api', ['unmap', keystroke, domain]); + }, + iunmap: (keystroke, domain) => { + dispatchSKEvent('api', ['iunmap', keystroke, domain]); + }, + vunmap: (keystroke, domain) => { + dispatchSKEvent('api', ['vunmap', keystroke, domain]); + }, + unmapAllExcept: (keystrokes, domain) => { + dispatchSKEvent('api', ['unmapAllExcept', keystrokes, domain]); + }, + removeSearchAlias: (alias, search_leader_key, only_this_site_key) => { + dispatchSKEvent('api', ['removeSearchAlias', alias, search_leader_key, only_this_site_key]); + }, + searchSelectedWith: (se, onlyThisSite, interactive, alias) => { + dispatchSKEvent('api', ['removeSearchAlias', se, onlyThisSite, interactive, alias]); + }, + tabOpenLink, + Clipboard: { + write: (text) => { + dispatchSKEvent('api', ['clipboard:write', text]); + }, + read: (cb) => { + onClipboardReadFn = cb; + dispatchSKEvent('api', ['clipboard:read']); + }, + }, + Hints: { + click: (links, force) => { + if (typeof(links) !== 'string') { + if (links instanceof HTMLElement) { + links = [links]; + } else if (links instanceof Array) { + links = links.filter((m) => m instanceof HTMLElement); + } else { + links = []; + } + if (links.length === 0) { + return; + } + links.forEach((m) => { + m.classList.add("surfingkeys--hints--clicking"); + }); + links = ".surfingkeys--hints--clicking"; + } + dispatchSKEvent('api', ['hints:click', links, force]); + }, + create: (cssSelector, onHintKey, attrs) => { + hintsFunction = onHintKey; + dispatchSKEvent('api', ['hints:create', cssSelector, "user", attrs]); + }, + dispatchMouseClick: (element) => { + dispatchSKEvent('hints', ['dispatchMouseClick'], element); + }, + style: (css, mode) => { + dispatchSKEvent('api', ['hints:style', css, mode]); + }, + setCharacters: (chars) => { + dispatchSKEvent('api', ['hints:setCharacters', chars]); + }, + setNumeric: () => { + dispatchSKEvent('api', ['hints:setNumeric']); + }, + }, + Normal: { + feedkeys: (keys) => { + dispatchSKEvent('api', ['normal:feedkeys', keys]); + }, + jumpVIMark: (mark) => { + dispatchSKEvent('api', ['normal:jumpVIMark', mark]); + }, + passThrough: (timeout) => { + dispatchSKEvent('api', ['normal:passThrough', timeout]); + }, + scroll: (type) => { + dispatchSKEvent('api', ['normal:scroll', type]); + }, + }, + Visual: { + style: (element, style) => { + dispatchSKEvent('api', ['visual:style', element, style]); + }, + }, + Front: { + registerInlineQuery: (args) => { + inlineQuery = args; + dispatchSKEvent('api', ['front:registerInlineQuery']); + }, + openOmnibar: (args) => { + dispatchSKEvent('api', ['front:openOmnibar', args]); + }, + showBanner, + showPopup + }, +}; + +export default (extensionRootUrl, uf) => { + EXTENSION_ROOT_URL = extensionRootUrl; + if (isInUIFrame()) return; + var settings = {}, error = ""; + try { + uf(api, settings); + } catch(e) { + error = e.toString(); + } + applyUserSettings({settings, error}); +}; diff --git a/tests/background/start.test.js b/tests/background/start.test.js index 211e35ac6..f017eace1 100644 --- a/tests/background/start.test.js +++ b/tests/background/start.test.js @@ -4,6 +4,9 @@ describe('background start', () => { beforeAll(async () => { global.chrome = { runtime: { + getManifest: () => { + return {manifest_version: 2}; + }, connectNative: () => { return { postMessage: jest.fn(), diff --git a/tests/content_scripts/common/normal.test.js b/tests/content_scripts/common/normal.test.js index 98e236543..49e50054b 100644 --- a/tests/content_scripts/common/normal.test.js +++ b/tests/content_scripts/common/normal.test.js @@ -25,8 +25,10 @@ describe('normal mode', () => { test("normal /", async () => { normal.enter(); await new Promise((r) => { - document.addEventListener("surfingkeys:openFinder", function(evt) { - r(evt); + document.addEventListener("surfingkeys:front", function(evt) { + if (evt.detail.length && evt.detail[0] === "openFinder") { + r(evt); + } }); document.body.dispatchEvent(new KeyboardEvent('keydown',{'key':'/'})); }); @@ -60,8 +62,10 @@ describe('normal mode', () => { test("normal mouse up", async () => { runtime.conf.mouseSelectToQuery = [ "http://localhost" ]; await new Promise((r) => { - document.addEventListener("surfingkeys:querySelectedWord", function(evt) { - r(evt); + document.addEventListener("surfingkeys:front", function(evt) { + if (evt.detail.length && evt.detail[0] === "querySelectedWord") { + r(evt); + } }); document.body.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, diff --git a/tests/content_scripts/markdown.test.js b/tests/content_scripts/markdown.test.js index e2e6dff49..d18ca65dc 100644 --- a/tests/content_scripts/markdown.test.js +++ b/tests/content_scripts/markdown.test.js @@ -5,7 +5,7 @@ import { waitForEvent } from '../utils'; describe('markdown viewer', () => { let dispatchSKEvent, createClipboard, createInsert, createNormal, - createHints, createVisual, createFront, createAPI; + createHints, createVisual, createFront, createAPI, createDefaultMappings; let normal, clipboard, api, Mode; @@ -18,6 +18,7 @@ describe('markdown viewer', () => { global.chrome = { runtime: { + getURL: jest.fn(), sendMessage: jest.fn(), onMessage: { addListener: jest.fn() @@ -27,9 +28,6 @@ describe('markdown viewer', () => { local: { get: jest.fn() } - }, - extension: { - getURL: jest.fn() } } global.DOMRect = jest.fn(); @@ -45,6 +43,7 @@ describe('markdown viewer', () => { createVisual = require('src/content_scripts/common/visual.js').default; createFront = require('src/content_scripts/front.js').default; createAPI = require('src/content_scripts/common/api.js').default; + createDefaultMappings = require('src/content_scripts/common/default.js').default; require('src/content_scripts/markdown'); Mode.init(); @@ -57,6 +56,7 @@ describe('markdown viewer', () => { const visual = createVisual(clipboard, hints); const front = createFront(insert, normal, hints, visual); api = createAPI(clipboard, insert, normal, hints, visual, front, {}); + createDefaultMappings(api, clipboard, insert, normal, hints, visual, front); }); test("verify local shortcuts for markdown preview", async () => { diff --git a/tests/content_scripts/ui/frontend.test.js b/tests/content_scripts/ui/frontend.test.js index 5646cfbf2..6bdee7f03 100644 --- a/tests/content_scripts/ui/frontend.test.js +++ b/tests/content_scripts/ui/frontend.test.js @@ -13,10 +13,13 @@ describe('ui front', () => { runtime: { onMessage: { addListener: jest.fn() - } - }, - extension: { + }, getURL: jest.fn() + }, + storage: { + local: { + get: jest.fn() + } } } global.DOMRect = jest.fn(); diff --git a/tests/content_scripts/ui/omnibar.test.js b/tests/content_scripts/ui/omnibar.test.js index 45e9fecee..b6e540ed4 100644 --- a/tests/content_scripts/ui/omnibar.test.js +++ b/tests/content_scripts/ui/omnibar.test.js @@ -13,10 +13,13 @@ describe('ui omnibar', () => { sendMessage: jest.fn(), onMessage: { addListener: jest.fn() - } - }, - extension: { + }, getURL: jest.fn() + }, + storage: { + local: { + get: jest.fn() + } } } global.DOMRect = jest.fn();