diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/clipboard.coffee | 12 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 126 | ||||
| -rw-r--r-- | lib/find_mode_history.coffee | 3 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 14 | ||||
| -rw-r--r-- | lib/keyboard_utils.coffee | 178 | ||||
| -rw-r--r-- | lib/rect.coffee | 11 | ||||
| -rw-r--r-- | lib/settings.coffee | 111 | ||||
| -rw-r--r-- | lib/utils.coffee | 77 |
8 files changed, 314 insertions, 218 deletions
diff --git a/lib/clipboard.coffee b/lib/clipboard.coffee index d015cdad..11542b11 100644 --- a/lib/clipboard.coffee +++ b/lib/clipboard.coffee @@ -1,8 +1,9 @@ Clipboard = - _createTextArea: -> - textArea = document.createElement "textarea" + _createTextArea: (tagName = "textarea") -> + textArea = document.createElement tagName textArea.style.position = "absolute" textArea.style.left = "-100%" + textArea.contentEditable = "true" textArea # http://groups.google.com/group/chromium-extensions/browse_thread/thread/49027e7f3b04f68/f6ab2457dee5bf55 @@ -16,14 +17,15 @@ Clipboard = document.body.removeChild(textArea) paste: -> - textArea = @_createTextArea() + textArea = @_createTextArea "div" # Use a <div> so Firefox pastes rich text. document.body.appendChild(textArea) textArea.focus() document.execCommand("Paste") - value = textArea.value + value = textArea.innerText document.body.removeChild(textArea) value.replace /\xa0/g, " " -root = exports ? window +root = exports ? (window.root ?= {}) root.Clipboard = Clipboard +extend window, root unless exports? diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 8e953405..c8f2e553 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -5,7 +5,7 @@ DomUtils = documentReady: do -> [isReady, callbacks] = [document.readyState != "loading", []] unless isReady - window.addEventListener "DOMContentLoaded", onDOMContentLoaded = -> + window.addEventListener "DOMContentLoaded", onDOMContentLoaded = forTrusted -> window.removeEventListener "DOMContentLoaded", onDOMContentLoaded isReady = true callback() for callback in callbacks @@ -16,7 +16,7 @@ DomUtils = documentComplete: do -> [isComplete, callbacks] = [document.readyState == "complete", []] unless isComplete - window.addEventListener "load", onLoad = -> + window.addEventListener "load", onLoad = forTrusted -> window.removeEventListener "load", onLoad isComplete = true callback() for callback in callbacks @@ -219,7 +219,7 @@ DomUtils = node = selection.anchorNode node and @isDOMDescendant element, node else - if selection.type == "Range" and selection.isCollapsed + if DomUtils.getSelectionType(selection) == "Range" and selection.isCollapsed # The selection is inside the Shadow DOM of a node. We can check the node it registers as being # before, since this represents the node whose Shadow DOM it's inside. containerNode = selection.anchorNode.childNodes[selection.anchorOffset] @@ -246,10 +246,23 @@ DomUtils = if element.selectionStart == 0 and element.selectionEnd == 0 element.setSelectionRange element.value.length, element.value.length - simulateClick: (element, modifiers) -> + simulateClick: (element, modifiers = {}) -> eventSequence = ["mouseover", "mousedown", "mouseup", "click"] for event in eventSequence - @simulateMouseEvent event, element, modifiers + defaultActionShouldTrigger = + if Utils.isFirefox() and Object.keys(modifiers).length == 0 and event == "click" and + element.target == "_blank" and element.href and + not element.hasAttribute("onclick") and not element.hasAttribute("_vimium-has-onclick-listener") + # Simulating a click on a target "_blank" element triggers the Firefox popup blocker. + # Note(smblott) This will be incorrect if there is a click listener on the element. + true + else + @simulateMouseEvent event, element, modifiers + if event == "click" and defaultActionShouldTrigger and Utils.isFirefox() + # Firefox doesn't (currently) trigger the default action for modified keys. + if 0 < Object.keys(modifiers).length or element.target == "_blank" + DomUtils.simulateClickDefaultAction element, modifiers + defaultActionShouldTrigger # return the values returned by each @simulateMouseEvent call. simulateMouseEvent: do -> lastHoveredElement = undefined @@ -272,6 +285,29 @@ DomUtils = # but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately element.dispatchEvent(mouseEvent) + simulateClickDefaultAction: (element, modifiers = {}) -> + return unless element.tagName?.toLowerCase() == "a" and element.href? + + {ctrlKey, shiftKey, metaKey, altKey} = modifiers + + # Mac uses a different new tab modifier (meta vs. ctrl). + if KeyboardUtils.platform == "Mac" + newTabModifier = metaKey == true and ctrlKey == false + else + newTabModifier = metaKey == false and ctrlKey == true + + if newTabModifier + # Open in new tab. Shift determines whether the tab is focused when created. Alt is ignored. + chrome.runtime.sendMessage {handler: "openUrlInNewTab", url: element.href, active: + shiftKey == true} + else if shiftKey == true and metaKey == false and ctrlKey == false and altKey == false + # Open in new window. + chrome.runtime.sendMessage {handler: "openUrlInNewWindow", url: element.href} + else if element.target == "_blank" + chrome.runtime.sendMessage {handler: "openUrlInNewTab", url: element.href, active: true} + + return + addFlashRect: (rect) -> flashEl = @createElement "div" flashEl.classList.add "vimiumReset" @@ -288,6 +324,25 @@ DomUtils = flashEl = @addFlashRect rect setTimeout((-> DomUtils.removeElement flashEl), 400) + getViewportTopLeft: -> + box = document.documentElement + style = getComputedStyle box + rect = box.getBoundingClientRect() + if style.position == "static" and not /content|paint|strict/.test(style.contain or "") + # The margin is included in the client rect, so we need to subtract it back out. + marginTop = parseInt style.marginTop + marginLeft = parseInt style.marginLeft + top: -rect.top + marginTop, left: -rect.left + marginLeft + else + if Utils.isFirefox() + # These are always 0 for documentElement on Firefox, so we derive them from CSS border. + clientTop = parseInt style.borderTopWidth + clientLeft = parseInt style.borderLeftWidth + else + {clientTop, clientLeft} = box + top: -rect.top - clientTop, left: -rect.left - clientLeft + + suppressPropagation: (event) -> event.stopImmediatePropagation() @@ -295,20 +350,50 @@ DomUtils = event.preventDefault() @suppressPropagation(event) - # Suppress the next keyup event for Escape. - suppressKeyupAfterEscape: (handlerStack) -> - handlerStack.push - _name: "dom_utils/suppressKeyupAfterEscape" - keyup: (event) -> - return true unless KeyboardUtils.isEscape event - @remove() - false + consumeKeyup: do -> + handlerId = null + + (event, callback = null, suppressPropagation) -> + unless event.repeat + handlerStack.remove handlerId if handlerId? + code = event.code + handlerId = handlerStack.push + _name: "dom_utils/consumeKeyup" + keyup: (event) -> + return handlerStack.continueBubbling unless event.code == code + @remove() + if suppressPropagation + DomUtils.suppressPropagation event + else + DomUtils.suppressEvent event + handlerStack.continueBubbling + # We cannot track keyup events if we lose the focus. + blur: (event) -> + @remove() if event.target == window + handlerStack.continueBubbling + callback?() + if suppressPropagation + DomUtils.suppressPropagation event + handlerStack.suppressPropagation + else + DomUtils.suppressEvent event + handlerStack.suppressEvent + + # Polyfill for selection.type (which is not available in Firefox). + getSelectionType: (selection = document.getSelection()) -> + selection.type or do -> + if selection.rangeCount == 0 + "None" + else if selection.isCollapsed + "Caret" + else + "Range" # Adapted from: http://roysharon.com/blog/37. # This finds the element containing the selection focus. getElementWithFocus: (selection, backwards) -> r = t = selection.getRangeAt 0 - if selection.type == "Range" + if DomUtils.getSelectionType(selection) == "Range" r = t.cloneRange() r.collapse backwards t = r.startContainer @@ -334,11 +419,20 @@ DomUtils = # If the element is rendered in a shadow DOM via a <content> element, the <content> element will be # returned, so the shadow DOM is traversed rather than passed over. getContainingElement: (element) -> - element.getDestinationInsertionPoints()[0] or element.parentElement + element.getDestinationInsertionPoints?()[0] or element.parentElement # This tests whether a window is too small to be useful. windowIsTooSmall: -> return window.innerWidth < 3 or window.innerHeight < 3 -root = exports ? window + # Inject user styles manually. This is only necessary for our chrome-extension:// pages and frames. + injectUserCss: -> + Settings.onLoaded -> + style = document.createElement "style" + style.type = "text/css" + style.textContent = Settings.get "userDefinedLinkHintCss" + document.head.appendChild style + +root = exports ? (window.root ?= {}) root.DomUtils = DomUtils +extend window, root unless exports? diff --git a/lib/find_mode_history.coffee b/lib/find_mode_history.coffee index ff660bd2..93698266 100644 --- a/lib/find_mode_history.coffee +++ b/lib/find_mode_history.coffee @@ -46,5 +46,6 @@ FindModeHistory = refreshRawQueryList: (query, rawQueryList) -> ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max] -root = exports ? window +root = exports ? (window.root ?= {}) root.FindModeHistory = FindModeHistory +extend window, root unless exports? diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee index 806b707f..a43fc356 100644 --- a/lib/handler_stack.coffee +++ b/lib/handler_stack.coffee @@ -1,4 +1,4 @@ -root = exports ? window +root = exports ? (window.root ?= {}) class HandlerStack constructor: -> @@ -57,7 +57,10 @@ class HandlerStack if result == @passEventToPage return true else if result == @suppressPropagation - DomUtils.suppressPropagation event + if type == "keydown" + DomUtils.consumeKeyup event, null, true + else + DomUtils.suppressPropagation event return false else if result == @restartBubbling return @bubbleEvent type, event @@ -65,7 +68,11 @@ class HandlerStack true # Do nothing, but continue bubbling. else # result is @suppressEvent or falsy. - DomUtils.suppressEvent event if @isChromeEvent event + if @isChromeEvent event + if type == "keydown" + DomUtils.consumeKeyup event + else + DomUtils.suppressEvent event return false # None of our handlers care about this event, so pass it to the page. @@ -120,3 +127,4 @@ class HandlerStack root.HandlerStack = HandlerStack root.handlerStack = new HandlerStack() +extend window, root unless exports? diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee index f997b455..673289b9 100644 --- a/lib/keyboard_utils.coffee +++ b/lib/keyboard_utils.coffee @@ -1,26 +1,11 @@ -KeyboardUtils = - keyCodes: - { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, ctrlEnter: 10, space: 32, shiftKey: 16, ctrlKey: 17, f1: 112, - f12: 123, tab: 9, downArrow: 40, upArrow: 38 } +mapKeyRegistry = {} +# NOTE: "?" here for the tests. +Utils?.monitorChromeStorage "mapKeyRegistry", (value) => mapKeyRegistry = value +KeyboardUtils = + # This maps event.key key names to Vimium key names. keyNames: - { 37: "left", 38: "up", 39: "right", 40: "down", 32: "space" } - - # This is a mapping of the incorrect keyIdentifiers generated by Webkit on Windows during keydown events to - # the correct identifiers, which are correctly generated on Mac. We require this mapping to properly handle - # these keys on Windows. See https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. - keyIdentifierCorrectionMap: - "U+00C0": ["U+0060", "U+007E"] # `~ - "U+00BD": ["U+002D", "U+005F"] # -_ - "U+00BB": ["U+003D", "U+002B"] # =+ - "U+00DB": ["U+005B", "U+007B"] # [{ - "U+00DD": ["U+005D", "U+007D"] # ]} - "U+00DC": ["U+005C", "U+007C"] # \| - "U+00BA": ["U+003B", "U+003A"] # ;: - "U+00DE": ["U+0027", "U+0022"] # '" - "U+00BC": ["U+002C", "U+003C"] # ,< - "U+00BE": ["U+002E", "U+003E"] # .> - "U+00BF": ["U+002F", "U+003F"] # /? + "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space" init: -> if (navigator.userAgent.indexOf("Mac") != -1) @@ -30,90 +15,95 @@ KeyboardUtils = else @platform = "Windows" - # We are migrating from using event.keyIdentifier to using event.key. For some period of time, we must - # support both. This wrapper can be removed once Chrome 52 is considered too old to support. getKeyChar: (event) -> - # We favor using event.keyIdentifier due to Chromium's currently (Chrome 51) incorrect implementataion of - # event.key; see #2147. - if event.keyIdentifier? - @getKeyCharUsingKeyIdentifier event - else - @getKeyCharUsingKey event - - getKeyCharUsingKey: (event) -> - if event.keyCode of @keyNames - @keyNames[event.keyCode] - else if event.key.length == 1 - event.key - else if event.key.length == 2 and "F1" <= event.key <= "F9" - event.key.toLowerCase() # F1 to F9. - else if event.key.length == 3 and "F10" <= event.key <= "F12" - event.key.toLowerCase() # F10 to F12. + unless Settings.get "ignoreKeyboardLayout" + key = event.key + else unless event.code + key = "" + else if event.code[...6] == "Numpad" + # We cannot correctly emulate the numpad, so fall back to event.key; see #2626. + key = event.key else + # The logic here is from the vim-like-key-notation project (https://github.com/lydell/vim-like-key-notation). + key = event.code + key = key[3..] if key[...3] == "Key" + # Translate some special keys to event.key-like strings and handle <Shift>. + if @enUsTranslations[key] + key = if event.shiftKey then @enUsTranslations[key][1] else @enUsTranslations[key][0] + else if key.length == 1 and not event.shiftKey + key = key.toLowerCase() + + # It appears that key is not always defined (see #2453). + unless key "" + else if key of @keyNames + @keyNames[key] + else if @isModifier event + "" # Don't resolve modifier keys. + else if key.length == 1 + key + else + key.toLowerCase() - getKeyCharUsingKeyIdentifier: (event) -> - # Not a letter - if (event.keyIdentifier.slice(0, 2) != "U+") - return @keyNames[event.keyCode] if (@keyNames[event.keyCode]) - # F-key - if (event.keyCode >= @keyCodes.f1 && event.keyCode <= @keyCodes.f12) - return "f" + (1 + event.keyCode - keyCodes.f1) - return "" - - keyIdentifier = event.keyIdentifier - # On Windows, the keyIdentifiers for non-letter keys are incorrect. See - # https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. - if ((@platform == "Windows" || @platform == "Linux") && @keyIdentifierCorrectionMap[keyIdentifier]) - correctedIdentifiers = @keyIdentifierCorrectionMap[keyIdentifier] - keyIdentifier = if event.shiftKey then correctedIdentifiers[1] else correctedIdentifiers[0] - unicodeKeyInHex = "0x" + keyIdentifier.substring(2) - character = String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase() - if event.shiftKey then character.toUpperCase() else character - - isPrimaryModifierKey: (event) -> if (@platform == "Mac") then event.metaKey else event.ctrlKey + getKeyCharString: (event) -> + if keyChar = @getKeyChar event + modifiers = [] - isEscape: (event) -> - # c-[ is mapped to ESC in Vim by default. - (event.keyCode == @keyCodes.ESC) || (event.ctrlKey && @getKeyChar(event) == '[' and not event.metaKey) + keyChar = keyChar.toUpperCase() if event.shiftKey and keyChar.length == 1 + # These must be in alphabetical order (to match the sorted modifier order in Commands.normalizeKey). + modifiers.push "a" if event.altKey + modifiers.push "c" if event.ctrlKey + modifiers.push "m" if event.metaKey - # TODO. This is probably a poor way of detecting printable characters. However, it shouldn't incorrectly - # identify any of chrome's own keyboard shortcuts as printable. - isPrintable: (event) -> - return false if event.metaKey or event.ctrlKey or event.altKey - keyChar = - if event.type == "keypress" - String.fromCharCode event.charCode - else - @getKeyChar event - keyChar.length == 1 + keyChar = [modifiers..., keyChar].join "-" + keyChar = "<#{keyChar}>" if 1 < keyChar.length + keyChar = mapKeyRegistry[keyChar] ? keyChar + keyChar - # Return the Vimium key representation for this keyboard event. Return a falsy value (the empty string or - # undefined) when no Vimium representation is appropriate. - getKeyCharString: (event) -> - switch event.type - when "keypress" - # Ignore modifier keys by themselves. - if 31 < event.keyCode - String.fromCharCode event.charCode + isEscape: do -> + useVimLikeEscape = true + Utils.monitorChromeStorage "useVimLikeEscape", (value) -> useVimLikeEscape = value - when "keydown" - # Handle special keys and normal input keys with modifiers being pressed. - keyChar = @getKeyChar event - if 1 < keyChar.length or (keyChar.length == 1 and (event.metaKey or event.ctrlKey or event.altKey)) - modifiers = [] + (event) -> + # <c-[> is mapped to Escape in Vim by default. + event.key == "Escape" or (useVimLikeEscape and @getKeyCharString(event) == "<c-[>") - keyChar = keyChar.toUpperCase() if event.shiftKey - modifiers.push "m" if event.metaKey - modifiers.push "c" if event.ctrlKey - modifiers.push "a" if event.altKey + isBackspace: (event) -> + event.key in ["Backspace", "Delete"] - keyChar = [modifiers..., keyChar].join "-" - if 1 < keyChar.length then "<#{keyChar}>" else keyChar + isPrintable: (event) -> + @getKeyCharString(event)?.length == 1 + + isModifier: (event) -> + event.key in ["Control", "Shift", "Alt", "OS", "AltGraph", "Meta"] + + enUsTranslations: + "Backquote": ["`", "~"] + "Minus": ["-", "_"] + "Equal": ["=", "+"] + "Backslash": ["\\","|"] + "IntlBackslash": ["\\","|"] + "BracketLeft": ["[", "{"] + "BracketRight": ["]", "}"] + "Semicolon": [";", ":"] + "Quote": ["'", '"'] + "Comma": [",", "<"] + "Period": [".", ">"] + "Slash": ["/", "?"] + "Space": [" ", " "] + "Digit1": ["1", "!"] + "Digit2": ["2", "@"] + "Digit3": ["3", "#"] + "Digit4": ["4", "$"] + "Digit5": ["5", "%"] + "Digit6": ["6", "^"] + "Digit7": ["7", "&"] + "Digit8": ["8", "*"] + "Digit9": ["9", "("] + "Digit0": ["0", ")"] KeyboardUtils.init() -root = exports ? window +root = exports ? (window.root ?= {}) root.KeyboardUtils = KeyboardUtils -# TODO(philc): A lot of code uses this keyCodes hash... maybe we shouldn't export it as a global. -root.keyCodes = KeyboardUtils.keyCodes +extend window, root unless exports? diff --git a/lib/rect.coffee b/lib/rect.coffee index 0c67d287..0e9c3417 100644 --- a/lib/rect.coffee +++ b/lib/rect.coffee @@ -67,12 +67,18 @@ Rect = rects.filter (rect) -> rect.height > 0 and rect.width > 0 - contains: (rect1, rect2) -> + # Determine whether two rects overlap. + intersects: (rect1, rect2) -> rect1.right > rect2.left and rect1.left < rect2.right and rect1.bottom > rect2.top and rect1.top < rect2.bottom + # Determine whether two rects overlap, including 0-width intersections at borders. + intersectsStrict: (rect1, rect2) -> + rect1.right >= rect2.left and rect1.left <= rect2.right and + rect1.bottom >= rect2.top and rect1.top <= rect2.bottom + equals: (rect1, rect2) -> for property in ["top", "bottom", "left", "right", "width", "height"] return false if rect1[property] != rect2[property] @@ -82,5 +88,6 @@ Rect = @create (Math.max rect1.left, rect2.left), (Math.max rect1.top, rect2.top), (Math.min rect1.right, rect2.right), (Math.min rect1.bottom, rect2.bottom) -root = exports ? window +root = exports ? (window.root ?= {}) root.Rect = Rect +extend window, root unless exports? diff --git a/lib/settings.coffee b/lib/settings.coffee index 92871ee2..4bfbcbf4 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -10,9 +10,15 @@ # # In all cases except Settings.defaults, values are stored as jsonified strings. +# If the current frame is the Vomnibar or the HUD, then we'll need our Chrome stubs for the tests. +# We use "try" because this fails within iframes on Firefox (where failure doesn't actually matter). +try window.chrome ?= window.top?.chrome + +storageArea = if chrome.storage.sync? then "sync" else "local" + Settings = debug: false - storage: chrome.storage.sync + storage: chrome.storage[storageArea] cache: {} isLoaded: false onLoadedCallbacks: [] @@ -25,16 +31,26 @@ Settings = @cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage @runOnLoadedCallbacks() - chrome.storage.local.get null, (localItems) => - localItems = {} if chrome.runtime.lastError - @storage.get null, (syncedItems) => - unless chrome.runtime.lastError - @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems + # Test chrome.storage.sync to see if it is enabled. + # NOTE(mrmr1993, 2017-04-18): currently the API is defined in FF, but it is disabled behind a flag in + # about:config. Every use sets chrome.runtime.lastError, so we use that to check whether we can use it. + chrome.storage.sync.get null, => + if chrome.runtime.lastError + storageArea = "local" + @storage = chrome.storage[storageArea] - chrome.storage.onChanged.addListener (changes, area) => - @propagateChangesFromChromeStorage changes if area == "sync" + # Delay this initialisation until after the correct storage area is known. The significance of this is + # that it delays the on-loaded callbacks. + chrome.storage.local.get null, (localItems) => + localItems = {} if chrome.runtime.lastError + @storage.get null, (syncedItems) => + unless chrome.runtime.lastError + @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems - @runOnLoadedCallbacks() + chrome.storage.onChanged.addListener (changes, area) => + @propagateChangesFromChromeStorage changes if area == storageArea + + @runOnLoadedCallbacks() # Called after @cache has been initialized. On extension pages, this will be called twice, but that does # not matter because it's idempotent. @@ -71,12 +87,15 @@ Settings = if @shouldSyncKey key if shouldSetInSyncedStorage setting = {}; setting[key] = @cache[key] - @log " chrome.storage.sync.set(#{key})" + @log " chrome.storage.#{storageArea}.set(#{key})" @storage.set setting - if Utils.isBackgroundPage() + if Utils.isBackgroundPage() and storageArea == "sync" # Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below. @log " chrome.storage.local.remove(#{key})" chrome.storage.local.remove key + # NOTE(mrmr1993): In FF, |value| will be garbage collected when the page owning it is unloaded. + # Any postUpdateHooks that can be called from the options page/exclusions popup should be careful not to + # use |value| asynchronously, or else it may refer to a |DeadObject| and accesses will throw an error. @performPostUpdateHook key, value clear: (key) -> @@ -98,7 +117,7 @@ Settings = nuke: (key) -> delete localStorage[key] chrome.storage.local.remove key - chrome.storage.sync.remove key + chrome.storage.sync?.remove key # For development only. log: (args...) -> @@ -153,23 +172,23 @@ Settings = # put in an example search engine searchEngines: """ - w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia + w: https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia # More examples. # # (Vimium supports search completion Wikipedia, as # above, and for these.) # - # g: http://www.google.com/search?q=%s Google - # l: http://www.google.com/search?q=%s&btnI I'm feeling lucky... - # y: http://www.youtube.com/results?search_query=%s Youtube + # g: https://www.google.com/search?q=%s Google + # l: https://www.google.com/search?q=%s&btnI I'm feeling lucky... + # y: https://www.youtube.com/results?search_query=%s Youtube # gm: https://www.google.com/maps?q=%s Google maps # b: https://www.bing.com/search?q=%s Bing # d: https://duckduckgo.com/?q=%s DuckDuckGo - # az: http://www.amazon.com/s/?field-keywords=%s Amazon + # az: https://www.amazon.com/s/?field-keywords=%s Amazon # qw: https://www.qwant.com/?q=%s Qwant """ - newTabUrl: "chrome://newtab" + newTabUrl: "about:newtab" grabBackFocus: false regexFindMode: false waitForEnterForFilteredHints: false # Note: this defaults to true for new users; see below. @@ -178,46 +197,30 @@ Settings = helpDialog_showAdvancedCommands: false optionsPage_showAdvancedOptions: false passNextKeyKeys: [] + ignoreKeyboardLayout: false Settings.init() # Perform migration from old settings versions, if this is the background page. if Utils.isBackgroundPage() + Settings.applyMigrations = -> + unless Settings.get "settingsVersion" + # This is a new install. For some settings, we retain a legacy default behaviour for existing users but + # use a non-default behaviour for new users. + + # For waitForEnterForFilteredHints, "true" gives a better UX; see #1950. However, forcing the change on + # existing users would be unnecessarily disruptive. So, only new users default to "true". + Settings.set "waitForEnterForFilteredHints", true + + # We use settingsVersion to coordinate any necessary schema changes. + Settings.set("settingsVersion", Utils.getCurrentVersion()) + + # Remove legacy key which was used to control storage migration. This was after 1.57 (2016-10-01), and can + # be removed after 1.58 has been out for sufficiently long. + Settings.nuke "copyNonDefaultsToChromeStorage-20150717" + + Settings.onLoaded Settings.applyMigrations.bind Settings - if not Settings.get "settingsVersion" - # This is a new install. For some settings, we retain a legacy default behaviour for existing users but - # use a non-default behaviour for new users. - - # For waitForEnterForFilteredHints, we (smblott) think that "true" gives a better UX; see #1950. However, - # forcing the change on existing users would be unnecessarily disruptive. So, only new users default to - # "true". - Settings.set "waitForEnterForFilteredHints", true - - # We use settingsVersion to coordinate any necessary schema changes. - Settings.set("settingsVersion", Utils.getCurrentVersion()) - - # In 1.46 we migrated the old "excludedUrls" setting to the new "exclusionRules" setting. And we kept a - # backup in "excludedUrlsBackup". Now (post 1.54, post 2016-02-12) we can clear up that backup (and any - # extraordinalrily old "excludedUrls" setting). - Settings.nuke "excludedUrlsBackup" - Settings.nuke "excludedUrls" - - # Migration (post 1.54, post 2016-2-12). Nuke legacy "findModeRawQuery" setting. - Settings.nuke "findModeRawQuery" - - # Migration (after 1.51, 2015/6/17). - # Copy options with non-default values (and which are not in synced storage) to chrome.storage.local; - # thereby making these settings accessible within content scripts. - do (migrationKey = "copyNonDefaultsToChromeStorage-20150717") -> - unless localStorage[migrationKey] - chrome.storage.sync.get null, (items) -> - unless chrome.runtime.lastError - updates = {} - for own key of localStorage - if Settings.shouldSyncKey(key) and not items[key] - updates[key] = localStorage[key] - chrome.storage.local.set updates, -> - localStorage[migrationKey] = not chrome.runtime.lastError - -root = exports ? window +root = exports ? (window.root ?= {}) root.Settings = Settings +extend window, root unless exports? diff --git a/lib/utils.coffee b/lib/utils.coffee index c06d8ac5..6f38be8f 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -1,4 +1,27 @@ +# Only pass events to the handler if they are marked as trusted by the browser. +# This is kept in the global namespace for brevity and ease of use. +window.forTrusted ?= (handler) -> (event) -> + if event?.isTrusted + handler.apply this, arguments + else + true + +browserInfo = browser?.runtime?.getBrowserInfo?() + Utils = + isFirefox: do -> + # NOTE(mrmr1993): This test only works in the background page, this is overwritten by isEnabledForUrl for + # content scripts. + isFirefox = false + browserInfo?.then? (browserInfo) -> + isFirefox = browserInfo?.name == "Firefox" + -> isFirefox + firefoxVersion: do -> + # NOTE(mrmr1993): This only works in the background page. + ffVersion = undefined + browserInfo?.then? (browserInfo) -> + ffVersion = browserInfo?.version + -> ffVersion getCurrentVersion: -> chrome.runtime.getManifest().version @@ -9,14 +32,6 @@ Utils = # Returns true whenever the current page is the extension's background page. isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage?() == window - # Takes a dot-notation object string and calls the function that it points to with the correct value for - # 'this'. - invokeCommandString: (str, args...) -> - [names..., name] = str.split '.' - obj = window - obj = obj[component] for component in names - obj[name].apply obj, args - # Escape all special characters, so RegExp will parse the string 'as is'. # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex escapeRegexSpecialCharacters: do -> @@ -41,7 +56,7 @@ Utils = url.startsWith "javascript:" hasFullUrlPrefix: do -> - urlPrefix = new RegExp "^[a-z]{3,}://." + urlPrefix = new RegExp "^[a-z][-+.a-z0-9]{2,}://." (url) -> urlPrefix.test url # Decode valid escape sequences in a URI. This is intended to mimic the best-effort decoding @@ -209,38 +224,12 @@ Utils = makeIdempotent: (func) -> (args...) -> ([previousFunc, func] = [func, null])[0]? args... -# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the -# search-engine configuration is unchanged. -SearchEngines = - previousSearchEngines: null - searchEngines: null - - refresh: (searchEngines) -> - unless @previousSearchEngines? and searchEngines == @previousSearchEngines - @previousSearchEngines = searchEngines - @searchEngines = new AsyncDataFetcher (callback) -> - engines = {} - for line in searchEngines.split "\n" - line = line.trim() - continue if /^[#"]/.test line - tokens = line.split /\s+/ - continue unless 2 <= tokens.length - keyword = tokens[0].split(":")[0] - searchUrl = tokens[1] - description = tokens[2..].join(" ") || "search (#{keyword})" - continue unless Utils.hasFullUrlPrefix searchUrl - engines[keyword] = { keyword, searchUrl, description } - - callback engines - - # Use the parsed search-engine configuration, possibly asynchronously. - use: (callback) -> - @searchEngines.use callback - - # Both set (refresh) the search-engine configuration and use it at the same time. - refreshAndUse: (searchEngines, callback) -> - @refresh searchEngines - @use callback + monitorChromeStorage: (key, setter) -> + # NOTE: "?" here for the tests. + chrome?.storage.local.get key, (obj) => + setter obj[key] if obj[key]? + chrome.storage.onChanged.addListener (changes, area) => + setter changes[key].newValue if changes[key]?.newValue? # This creates a new function out of an existing function, where the new function takes fewer arguments. This # allows us to pass around functions instead of functions + a partial list of arguments. @@ -338,9 +327,11 @@ class JobRunner onReady: (callback) -> @fetcher.use callback -root = exports ? window +root = exports ? (window.root ?= {}) root.Utils = Utils -root.SearchEngines = SearchEngines root.SimpleCache = SimpleCache root.AsyncDataFetcher = AsyncDataFetcher root.JobRunner = JobRunner +unless exports? + root.extend = extend + extend window, root |
