From 5885bc779bed46dfcff4b1e82968151448569f9f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 19 Aug 2014 18:15:13 +0100 Subject: Move the vomnibar to an iframe --- .gitignore | 2 + background_scripts/main.coffee | 39 +++-- content_scripts/vimium.css | 124 ++------------- content_scripts/vimium_frontend.coffee | 2 + content_scripts/vomnibar.coffee | 273 ++++++--------------------------- manifest.json | 5 +- pages/vomnibar.coffee | 266 ++++++++++++++++++++++++++++++++ pages/vomnibar.css | 129 ++++++++++++++++ pages/vomnibar.html | 21 +++ 9 files changed, 499 insertions(+), 362 deletions(-) create mode 100644 pages/vomnibar.coffee create mode 100644 pages/vomnibar.css create mode 100644 pages/vomnibar.html diff --git a/.gitignore b/.gitignore index 9df0d559..2595878c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ *.sublime* node_modules/* dist +jscoverage.json +tags diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index dda1beae..49417d7a 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -119,7 +119,7 @@ root.addExcludedUrl = (url) -> continue # And just keep everything else. newExcludedUrls.push(spec) - + Settings.set("excludedUrls", newExcludedUrls.join("\n")) chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, @@ -635,6 +635,12 @@ getCurrFrameIndex = (frames) -> return i if frames[i].id == focusedFrame frames.length + 1 +# Send message back to the tab unchanged. +# Frames in the same tab can use this to communicate securely. +echo = (request, sender) -> + delete request.handler # No need to send this information + chrome.tabs.sendMessage(sender.tab.id, request) + # Port handler mapping portHandlers = keyDown: handleKeyDown, @@ -642,23 +648,24 @@ portHandlers = filterCompleter: filterCompleter sendRequestHandlers = - getCompletionKeys: getCompletionKeysRequest, - getCurrentTabUrl: getCurrentTabUrl, - openUrlInNewTab: openUrlInNewTab, - openUrlInIncognito: openUrlInIncognito, - openUrlInCurrentTab: openUrlInCurrentTab, - openOptionsPageInNewTab: openOptionsPageInNewTab, - registerFrame: registerFrame, - frameFocused: handleFrameFocused, - upgradeNotificationClosed: upgradeNotificationClosed, - updateScrollPosition: handleUpdateScrollPosition, - copyToClipboard: copyToClipboard, - isEnabledForUrl: isEnabledForUrl, - saveHelpDialogSettings: saveHelpDialogSettings, - selectSpecificTab: selectSpecificTab, + getCompletionKeys: getCompletionKeysRequest + getCurrentTabUrl: getCurrentTabUrl + openUrlInNewTab: openUrlInNewTab + openUrlInIncognito: openUrlInIncognito + openUrlInCurrentTab: openUrlInCurrentTab + openOptionsPageInNewTab: openOptionsPageInNewTab + registerFrame: registerFrame + frameFocused: handleFrameFocused + upgradeNotificationClosed: upgradeNotificationClosed + updateScrollPosition: handleUpdateScrollPosition + copyToClipboard: copyToClipboard + isEnabledForUrl: isEnabledForUrl + saveHelpDialogSettings: saveHelpDialogSettings + selectSpecificTab: selectSpecificTab refreshCompleter: refreshCompleter - createMark: Marks.create.bind(Marks), + createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) + echo: echo # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 7998fe5c..fd613b31 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -266,136 +266,30 @@ div.vimiumHUD a.close-button:hover { body.vimiumFindMode ::selection { background: #ff9632; -}; +} -/* Vomnibar CSS */ +/* Vomnibar Frame CSS */ -#vomnibar ol, #vomnibar ul { - list-style: none; - display: block; -} +iframe.vomnibarFrame { + background-color: transparent; + padding: 0px; + overflow: hidden; -#vomnibar { display: block; position: fixed; - width: 80%; + width: calc(80% + 20px); /* same adjustment as in pages/vomnibar.coffee */ min-width: 400px; + height: calc(100% - 70px); top: 70px; left: 50%; margin: 0 0 0 -40%; + border: none; font-family: sans-serif; - background: #F1F1F1; - text-align: left; - border-radius: 4px; - box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); - border: 1px solid #aaa; /* One less than hint markers and the help dialog. */ z-index: 99999996; } -#vomnibar input { - color: #000; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 20px; - height: 34px; - margin-bottom: 0; - padding: 4px; - background-color: white; - border-radius: 3px; - border: 1px solid #E8E8E8; - box-shadow: #444 0px 0px 1px; - width: 100%; - outline: none; - box-sizing: border-box; -} - -#vomnibar .vomnibarSearchArea { - display: block; - padding: 10px; - background-color: #F1F1F1; - border-radius: 4px 4px 0 0; - border-bottom: 1px solid #C6C9CE; -} - -#vomnibar ul { - background-color: white; - border-radius: 0 0 4px 4px; - list-style: none; - padding: 10px 0; - padding-top: 0; -} - -#vomnibar li { - border-bottom: 1px solid #ddd; - line-height: 1.1em; - padding: 7px 10px; - font-size: 16px; - color: black; - position: relative; - display: list-item; - margin: auto; -} - -#vomnibar li:last-of-type { - border-bottom: none; -} - -#vomnibar li .vomnibarTopHalf, #vomnibar li .vomnibarBottomHalf { - display: block; - overflow: hidden; -} - -#vomnibar li .vomnibarBottomHalf { - font-size: 15px; - margin-top: 3px; - padding: 2px 0; -} - -#vomnibar li .vomnibarSource { - color: #777; - margin-right: 4px; -} -#vomnibar li .vomnibarRelevancy { - position: absolute; - right: 0; - top: 0; - padding: 5px; - background-color: white; - color: black; - font-family: monospace; - width: 100px; - overflow: hidden; -} - -#vomnibar li .vomnibarUrl { - white-space: nowrap; - color: #224684; -} - -#vomnibar li .vomnibarMatch { - font-weight: bold; - color: black; -} - -#vomnibar li em, #vomnibar li .vomnibarTitle { - color: black; - margin-left: 4px; - font-weight: normal; -} -#vomnibar li em { font-style: italic; } -#vomnibar li em .vomnibarMatch, #vomnibar li .vomnibarTitle .vomnibarMatch { - color: #333; - text-decoration: underline; -} - -#vomnibar li.vomnibarSelected { - background-color: #BBCEE9; - font-weight: normal; -} - - - div#vimiumFlash { box-shadow: 0px 0px 4px 2px #4183C4; padding: 1px; diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 137b9d1a..0a835de9 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -121,6 +121,8 @@ initializePreDomReady = -> getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue + vomnibarShow: -> Vomnibar.show() + vomnibarClose: -> Vomnibar.close() chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 9c13cd6d..0e19f194 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -1,243 +1,56 @@ Vomnibar = - vomnibarUI: null # the dialog instance for this window - completers: {} - - getCompleter: (name) -> - if (!(name of @completers)) - @completers[name] = new BackgroundCompleter(name) - @completers[name] - - # - # Activate the Vomnibox. - # - activateWithCompleter: (completerName, refreshInterval, initialQueryValue, selectFirstResult, forceNewTab) -> - completer = @getCompleter(completerName) - @vomnibarUI = new VomnibarUI() unless @vomnibarUI - completer.refresh() - @vomnibarUI.setInitialSelectionValue(if selectFirstResult then 0 else -1) - @vomnibarUI.setCompleter(completer) - @vomnibarUI.setRefreshInterval(refreshInterval) - @vomnibarUI.setForceNewTab(forceNewTab) - @vomnibarUI.show() - if (initialQueryValue) - @vomnibarUI.setQuery(initialQueryValue) - @vomnibarUI.update() - - activate: -> @activateWithCompleter("omni", 100) - activateInNewTab: -> @activateWithCompleter("omni", 100, null, false, true) - activateTabSelection: -> @activateWithCompleter("tabs", 0, null, true) - activateBookmarks: -> @activateWithCompleter("bookmarks", 0, null, true) - activateBookmarksInNewTab: -> @activateWithCompleter("bookmarks", 0, null, true, true) - getUI: -> @vomnibarUI - - -class VomnibarUI - constructor: -> - @refreshInterval = 0 - @initDom() + vomnibarElement: null + + activate: -> @open {completer:"omni"} + activateInNewTab: -> @open { + completer: "omni" + selectFirst: false + newTab: true + } + activateTabSelection: -> @open { + completer: "tabs" + selectFirst: true + } + activateBookmarks: -> @open { + completer: "bookmarks" + selectFirst: true + } + activateBookmarksInNewTab: -> @open { + completer: "bookmarks" + selectFirst: true + newTab: true + } + + open: (options) -> + unless @vomnibarElement? + @vomnibarElement = document.createElement "iframe" + @vomnibarElement.className = "vomnibarFrame" + @vomnibarElement.seamless = "seamless" + @hide() - setQuery: (query) -> @input.value = query + options.frameId = frameId - setInitialSelectionValue: (initialSelectionValue) -> - @initialSelectionValue = initialSelectionValue + optionStrings = [] + for option of options + if typeof options[option] == "boolean" + optionStrings.push option if options[option] + else + optionStrings.push "#{option}=#{escape(options[option])}" - setCompleter: (completer) -> - @completer = completer - @reset() + @vomnibarElement.src = "#{chrome.runtime.getURL "pages/vomnibar.html"}?#{optionStrings.join "&"}" + document.documentElement.appendChild @vomnibarElement - setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + @vomnibarElement.focus() - setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab + close: -> + @hide() + @vomnibarElement?.remove() show: -> - @box.style.display = "block" - @input.focus() - @handlerId = handlerStack.push keydown: @onKeydown.bind @ + @vomnibarElement?.style.display = "block" hide: -> - @box.style.display = "none" - @completionList.style.display = "none" - @input.blur() - handlerStack.remove @handlerId - - reset: -> - @input.value = "" - @updateTimer = null - @completions = [] - @selection = @initialSelectionValue - @update(true) - - updateSelection: -> - # We have taken the option to add some global state here (previousCompletionType) to tell if a search - # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1 - # I feel that this approach is cleaner than bubbling the state up from the suggestion level - # so we just inspect it afterwards - if @completions[0] - if @previousCompletionType != "search" && @completions[0].type == "search" - @selection = 0 - else if @previousCompletionType == "search" && @completions[0].type != "search" - @selection = -1 - for i in [0...@completionList.children.length] - @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") - @previousCompletionType = @completions[0].type if @completions[0] - - # - # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress. - # We support the arrow keys and other shortcuts for moving, so this method hides that complexity. - # - actionFromKeyEvent: (event) -> - key = KeyboardUtils.getKeyChar(event) - if (KeyboardUtils.isEscape(event)) - return "dismiss" - else if (key == "up" || - (event.shiftKey && event.keyCode == keyCodes.tab) || - (event.ctrlKey && (key == "k" || key == "p"))) - return "up" - else if (key == "down" || - (event.keyCode == keyCodes.tab && !event.shiftKey) || - (event.ctrlKey && (key == "j" || key == "n"))) - return "down" - else if (event.keyCode == keyCodes.enter) - return "enter" - - onKeydown: (event) -> - action = @actionFromKeyEvent(event) - return true unless action # pass through - - openInNewTab = @forceNewTab || - (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event)) - if (action == "dismiss") - @hide() - else if (action == "up") - @selection -= 1 - @selection = @completions.length - 1 if @selection < @initialSelectionValue - @input.value = @completions[@selection].url - @updateSelection() - else if (action == "down") - @selection += 1 - @selection = @initialSelectionValue if @selection == @completions.length - @input.value = @completions[@selection].url - @updateSelection() - else if (action == "enter") - # If they type something and hit enter without selecting a completion from our list of suggestions, - # try to open their query as a URL directly. If it doesn't look like a URL, we will search using - # google. - if (@selection == -1) - query = @input.value.trim() - # on an empty vomnibar is a no-op. - return unless 0 < query.length - @hide() - chrome.runtime.sendMessage({ - handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" - url: query }) - else - @update true, => - # Shift+Enter will open the result in a new tab instead of the current tab. - @completions[@selection].performAction(openInNewTab) - @hide() - - # It seems like we have to manually suppress the event here and still return true. - event.stopPropagation() - event.preventDefault() - true - - updateCompletions: (callback) -> - query = @input.value.trim() - - @completer.filter query, (completions) => - @completions = completions - @populateUiWithCompletions(completions) - callback() if callback - - populateUiWithCompletions: (completions) -> - # update completion list with the new data - @completionList.innerHTML = completions.map((completion) -> "
  • #{completion.html}
  • ").join("") - @completionList.style.display = if completions.length > 0 then "block" else "none" - @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) - @updateSelection() - - update: (updateSynchronously, callback) -> - if (updateSynchronously) - # cancel scheduled update - if (@updateTimer != null) - window.clearTimeout(@updateTimer) - @updateCompletions(callback) - else if (@updateTimer != null) - # an update is already scheduled, don't do anything - return - else - # always update asynchronously for better user experience and to take some load off the CPU - # (not every keystroke will cause a dedicated update) - @updateTimer = setTimeout(=> - @updateCompletions(callback) - @updateTimer = null - @refreshInterval) - - initDom: -> - @box = Utils.createElementFromHtml( - """ -
    -
    - -
    - -
    - """) - @box.style.display = "none" - document.body.appendChild(@box) - - @input = document.querySelector("#vomnibar input") - @input.addEventListener "input", => @update() - @completionList = document.querySelector("#vomnibar ul") - @completionList.style.display = "none" - -# -# Sends filter and refresh requests to a Vomnibox completer on the background page. -# -class BackgroundCompleter - # - name: The background page completer that you want to interface with. Either "omni", "tabs", or - # "bookmarks". */ - constructor: (@name) -> - @filterPort = chrome.runtime.connect({ name: "filterCompleter" }) - - refresh: -> chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name }) - - filter: (query, callback) -> - id = Utils.createUniqueId() - @filterPort.onMessage.addListener (msg) => - @filterPort.onMessage.removeListener(arguments.callee) - # The result objects coming from the background page will be of the form: - # { html: "", type: "", url: "" } - # type will be one of [tab, bookmark, history, domain]. - results = msg.results.map (result) -> - functionToCall = if (result.type == "tab") - BackgroundCompleter.completionActions.switchToTab.curry(result.tabId) - else - BackgroundCompleter.completionActions.navigateToUrl.curry(result.url) - result.performAction = functionToCall - result - callback(results) - - @filterPort.postMessage({ id: id, name: @name, query: query }) - -extend BackgroundCompleter, - # - # These are the actions we can perform when the user selects a result in the Vomnibox. - # - completionActions: - navigateToUrl: (url, openInNewTab) -> - # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab. - if url.startsWith "javascript:" - script = document.createElement 'script' - script.textContent = decodeURIComponent(url["javascript:".length..]) - (document.head || document.documentElement).appendChild script - else - chrome.runtime.sendMessage( - handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" - url: url, - selected: openInNewTab) - - switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) + @vomnibarElement?.style.display = "none" root = exports ? window root.Vomnibar = Vomnibar diff --git a/manifest.json b/manifest.json index 9a4e0167..84b91f0e 100644 --- a/manifest.json +++ b/manifest.json @@ -55,5 +55,8 @@ "browser_action": { "default_icon": "icons/browser_action_disabled.png", "default_popup": "pages/popup.html" - } + }, + "web_accessible_resources": [ + "pages/vomnibar.html" + ] } diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee new file mode 100644 index 00000000..11fb4b65 --- /dev/null +++ b/pages/vomnibar.coffee @@ -0,0 +1,266 @@ +Vomnibar = + vomnibarUI: null # the dialog instance for this window + completers: {} + + getCompleter: (name) -> + if (!(name of @completers)) + @completers[name] = new BackgroundCompleter(name) + @completers[name] + + # + # Activate the Vomnibox. + # + activateWithCompleter: (options) -> + completer = @getCompleter(options.completer) + @vomnibarUI ?= new VomnibarUI() + completer.refresh() + @vomnibarUI.setInitialSelectionValue(if options.selectFirst then 0 else -1) + @vomnibarUI.setCompleter(completer) + @vomnibarUI.setRefreshInterval(options.refreshInterval) + @vomnibarUI.setForceNewTab(options.newTab) + @vomnibarUI.setFrameId(options.frameId) + @vomnibarUI.show() + if (options.query) + @vomnibarUI.setQuery(options.query) + @vomnibarUI.update() + + getUI: -> @vomnibarUI + + +class VomnibarUI + constructor: -> + @refreshInterval = 0 + @initDom() + + setQuery: (query) -> @input.value = query + + setInitialSelectionValue: (initialSelectionValue) -> + @initialSelectionValue = initialSelectionValue + + setCompleter: (completer) -> + @completer = completer + @reset() + + setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + + setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab + + setFrameId: (frameId) -> @frameId = frameId + + show: -> + @box.style.display = "block" + @input.focus() + @input.addEventListener "keydown", @onKeydown + + chrome.runtime.sendMessage + handler: "echo" + name: "vomnibarShow" + frameId: @frameId + + hide: -> + @box.style.display = "none" + @completionList.style.display = "none" + @input.blur() + @input.removeEventListener "keydown", @onKeydown + window.parent.focus() + chrome.runtime.sendMessage + handler: "echo" + name: "vomnibarClose" + frameId: @frameId + + reset: -> + @input.value = "" + @updateTimer = null + @completions = [] + @selection = @initialSelectionValue + @update(true) + + updateSelection: -> + # We have taken the option to add some global state here (previousCompletionType) to tell if a search + # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1 + # I feel that this approach is cleaner than bubbling the state up from the suggestion level + # so we just inspect it afterwards + if @completions[0] + if @previousCompletionType != "search" && @completions[0].type == "search" + @selection = 0 + else if @previousCompletionType == "search" && @completions[0].type != "search" + @selection = -1 + for i in [0...@completionList.children.length] + @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") + @previousCompletionType = @completions[0].type if @completions[0] + + # + # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress. + # We support the arrow keys and other shortcuts for moving, so this method hides that complexity. + # + actionFromKeyEvent: (event) -> + key = KeyboardUtils.getKeyChar(event) + if (KeyboardUtils.isEscape(event)) + return "dismiss" + else if (key == "up" || + (event.shiftKey && event.keyCode == keyCodes.tab) || + (event.ctrlKey && (key == "k" || key == "p"))) + return "up" + else if (key == "down" || + (event.keyCode == keyCodes.tab && !event.shiftKey) || + (event.ctrlKey && (key == "j" || key == "n"))) + return "down" + else if (event.keyCode == keyCodes.enter) + return "enter" + + onKeydown: (event) => + action = @actionFromKeyEvent(event) + return true unless action # pass through + + openInNewTab = @forceNewTab || + (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event)) + if (action == "dismiss") + @hide() + else if (action == "up") + @selection -= 1 + @selection = @completions.length - 1 if @selection < @initialSelectionValue + @updateSelection() + else if (action == "down") + @selection += 1 + @selection = @initialSelectionValue if @selection == @completions.length + @updateSelection() + else if (action == "enter") + # If they type something and hit enter without selecting a completion from our list of suggestions, + # try to open their query as a URL directly. If it doesn't look like a URL, we will search using + # google. + if (@selection == -1) + query = @input.value.trim() + # on an empty vomnibar is a no-op. + return unless 0 < query.length + @hide() + chrome.runtime.sendMessage({ + handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: query }) + else + @update true, => + # Shift+Enter will open the result in a new tab instead of the current tab. + @completions[@selection].performAction(openInNewTab) + @hide() + + # It seems like we have to manually suppress the event here and still return true. + event.stopPropagation() + event.preventDefault() + true + + updateCompletions: (callback) -> + query = @input.value.trim() + + @completer.filter query, (completions) => + @completions = completions + @populateUiWithCompletions(completions) + callback() if callback + + populateUiWithCompletions: (completions) -> + # update completion list with the new data + @completionList.innerHTML = completions.map((completion) -> "
  • #{completion.html}
  • ").join("") + @completionList.style.display = if completions.length > 0 then "block" else "none" + @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) + @updateSelection() + + update: (updateSynchronously, callback) -> + if (updateSynchronously) + # cancel scheduled update + if (@updateTimer != null) + window.clearTimeout(@updateTimer) + @updateCompletions(callback) + else if (@updateTimer != null) + # an update is already scheduled, don't do anything + return + else + # always update asynchronously for better user experience and to take some load off the CPU + # (not every keystroke will cause a dedicated update) + @updateTimer = setTimeout(=> + @updateCompletions(callback) + @updateTimer = null + @refreshInterval) + + initDom: -> + @box = document.getElementById("vomnibar") + + @input = @box.querySelector("input") + @input.addEventListener "input", => @update() + @completionList = @box.querySelector("ul") + @completionList.style.display = "none" + + window.addEventListener "focus", => @input.focus() + +# +# Sends filter and refresh requests to a Vomnibox completer on the background page. +# +class BackgroundCompleter + # - name: The background page completer that you want to interface with. Either "omni", "tabs", or + # "bookmarks". */ + constructor: (@name) -> + @filterPort = chrome.runtime.connect({ name: "filterCompleter" }) + + refresh: -> chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name }) + + filter: (query, callback) -> + id = Utils.createUniqueId() + @filterPort.onMessage.addListener (msg) => + @filterPort.onMessage.removeListener(arguments.callee) + # The result objects coming from the background page will be of the form: + # { html: "", type: "", url: "" } + # type will be one of [tab, bookmark, history, domain]. + results = msg.results.map (result) -> + functionToCall = if (result.type == "tab") + BackgroundCompleter.completionActions.switchToTab.curry(result.tabId) + else + BackgroundCompleter.completionActions.navigateToUrl.curry(result.url) + result.performAction = functionToCall + result + callback(results) + + @filterPort.postMessage({ id: id, name: @name, query: query }) + +extend BackgroundCompleter, + # + # These are the actions we can perform when the user selects a result in the Vomnibox. + # + completionActions: + navigateToUrl: (url, openInNewTab) -> + # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open it in a new tab. + if url.startsWith "javascript:" + openInNewTab = false + chrome.runtime.sendMessage( + handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: url, + selected: openInNewTab) + + switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) + +initializeOnDomReady = -> + options = + completer: "omni" + query: null + frameId: -1 + + booleanOptions = ["selectFirst", "newTab"] + + # Convert options in URL to options object + document.location.search + .split(/[\?&]/) + .map((option) -> + [name, value] = option.split "=" + options[name] = value + ) + + # Set boolean options + for option in booleanOptions + options[option] = option of options and options[option] != "false" + + options.refreshInterval = switch options.completer + when "omni" then 100 + else 0 + + Vomnibar.activateWithCompleter options + +window.addEventListener "DOMContentLoaded", initializeOnDomReady + +root = exports ? window +root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.css b/pages/vomnibar.css new file mode 100644 index 00000000..958445e6 --- /dev/null +++ b/pages/vomnibar.css @@ -0,0 +1,129 @@ + +/* Vomnibar CSS */ + +#vomnibar ol, #vomnibar ul { + list-style: none; + display: block; +} + +#vomnibar { + display: block; + position: fixed; + width: calc(100% - 20px); /* adjusted to keep border radius and box-shadow visible*/ + /*min-width: 400px; + top: 70px; + left: 50%;*/ + top: 8px; + left: 8px; + /*margin: 0 0 0 -40%;*/ + font-family: sans-serif; + + background: #F1F1F1; + text-align: left; + border-radius: 4px; + box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); + border: 1px solid #aaa; + /* One less than hint markers and the help dialog. */ + z-index: 99999996; +} + +#vomnibar input { + color: #000; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 20px; + height: 34px; + margin-bottom: 0; + padding: 4px; + background-color: white; + border-radius: 3px; + border: 1px solid #E8E8E8; + box-shadow: #444 0px 0px 1px; + width: 100%; + outline: none; + box-sizing: border-box; +} + +#vomnibar .vomnibarSearchArea { + display: block; + padding: 10px; + background-color: #F1F1F1; + border-radius: 4px 4px 0 0; + border-bottom: 1px solid #C6C9CE; +} + +#vomnibar ul { + background-color: white; + border-radius: 0 0 4px 4px; + list-style: none; + padding: 10px 0; + padding-top: 0; +} + +#vomnibar li { + border-bottom: 1px solid #ddd; + line-height: 1.1em; + padding: 7px 10px; + font-size: 16px; + color: black; + position: relative; + display: list-item; + margin: auto; +} + +#vomnibar li:last-of-type { + border-bottom: none; +} + +#vomnibar li .vomnibarTopHalf, #vomnibar li .vomnibarBottomHalf { + display: block; + overflow: hidden; +} + +#vomnibar li .vomnibarBottomHalf { + font-size: 15px; + margin-top: 3px; + padding: 2px 0; +} + +#vomnibar li .vomnibarSource { + color: #777; + margin-right: 4px; +} +#vomnibar li .vomnibarRelevancy { + position: absolute; + right: 0; + top: 0; + padding: 5px; + background-color: white; + color: black; + font-family: monospace; + width: 100px; + overflow: hidden; +} + +#vomnibar li .vomnibarUrl { + white-space: nowrap; + color: #224684; +} + +#vomnibar li .vomnibarMatch { + font-weight: bold; + color: black; +} + +#vomnibar li em, #vomnibar li .vomnibarTitle { + color: black; + margin-left: 4px; + font-weight: normal; +} +#vomnibar li em { font-style: italic; } +#vomnibar li em .vomnibarMatch, #vomnibar li .vomnibarTitle .vomnibarMatch { + color: #333; + text-decoration: underline; +} + +#vomnibar li.vomnibarSelected { + background-color: #BBCEE9; + font-weight: normal; +} + diff --git a/pages/vomnibar.html b/pages/vomnibar.html new file mode 100644 index 00000000..6cba99e6 --- /dev/null +++ b/pages/vomnibar.html @@ -0,0 +1,21 @@ + + + Vomnibar + + + + + + + + + + +
    +
    + +
    +
      +
      + + -- cgit v1.2.3 From f9cb424fad03042353e8720398b390dd8b2281d0 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Date: Fri, 27 Dec 2013 10:05:41 +0530 Subject: Allow open popups from bookmarklets. Fix #968. Use the `openUrlInCurrentTab` message to open bookmarklets so they are opened via `chrome.tabs.update` instead of an injected script element. This allows bookmarklets to open popups. --- lib/utils.coffee | 2 +- pages/vomnibar.coffee | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/utils.coffee b/lib/utils.coffee index a93831d7..5d93ae70 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -26,7 +26,7 @@ Utils = -> id += 1 hasChromePrefix: (url) -> - chromePrefixes = [ "about", "view-source", "chrome-extension", "data" ] + chromePrefixes = ["about:", "view-source:", "chrome-extension:", "data:", "javascript:"] for prefix in chromePrefixes return true if url.startsWith prefix false diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 11fb4b65..5cd37db6 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -224,9 +224,8 @@ extend BackgroundCompleter, # completionActions: navigateToUrl: (url, openInNewTab) -> - # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open it in a new tab. - if url.startsWith "javascript:" - openInNewTab = false + # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab. + openInNewTab = false if url.startsWith("javascript:") chrome.runtime.sendMessage( handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" url: url, -- cgit v1.2.3 From fdf592b25ead4c4ec919c1d0e6322d2dba48c1f8 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 21 Aug 2014 19:50:09 +0100 Subject: Hacks to make Vomnibar tests work again --- pages/vomnibar.coffee | 19 +++++++++++++++++++ test_harnesses/vomnibar.html | 2 +- tests/dom_tests/dom_tests.html | 8 ++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 5cd37db6..2e51082a 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -24,6 +24,25 @@ Vomnibar = @vomnibarUI.setQuery(options.query) @vomnibarUI.update() + activate: -> @activateWithCompleter {completer:"omni"} + activateInNewTab: -> @activateWithCompleter { + completer: "omni" + selectFirst: false + newTab: true + } + activateTabSelection: -> @activateWithCompleter { + completer: "tabs" + selectFirst: true + } + activateBookmarks: -> @activateWithCompleter { + completer: "bookmarks" + selectFirst: true + } + activateBookmarksInNewTab: -> @activateWithCompleter { + completer: "bookmarks" + selectFirst: true + newTab: true + } getUI: -> @vomnibarUI diff --git a/test_harnesses/vomnibar.html b/test_harnesses/vomnibar.html index 4d50e749..820210b0 100644 --- a/test_harnesses/vomnibar.html +++ b/test_harnesses/vomnibar.html @@ -11,7 +11,7 @@ - + + @@ -53,5 +54,12 @@
      +
      +
      + +
      +
        +
        + -- cgit v1.2.3 From d65f265a6ad137be0db4d8c86879e5123a10087b Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 2 Sep 2014 17:43:41 +0100 Subject: Add comments about moving the Vomnibar to an iframe --- background_scripts/main.coffee | 4 ++-- content_scripts/vomnibar.coffee | 8 ++++++++ pages/vomnibar.coffee | 5 +++++ tests/dom_tests/dom_tests.html | 3 +++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 49417d7a..03d6143d 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -635,8 +635,8 @@ getCurrFrameIndex = (frames) -> return i if frames[i].id == focusedFrame frames.length + 1 -# Send message back to the tab unchanged. -# Frames in the same tab can use this to communicate securely. +# Send message back to the tab unchanged. This allows different frames from the same tab to message eachother +# while avoiding security concerns such as eavesdropping or message spoofing. echo = (request, sender) -> delete request.handler # No need to send this information chrome.tabs.sendMessage(sender.tab.id, request) diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 0e19f194..7b3cfdbe 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -1,3 +1,6 @@ +# +# This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar. +# Vomnibar = vomnibarElement: null @@ -21,6 +24,11 @@ Vomnibar = newTab: true } + # This function opens the vomnibar. It accepts options, a map with the values: + # completer - The completer to fetch results from. + # query - Optional. Text to prefill the Vomnibar with. + # selectFirst - Optional, boolean. Whether to select the first entry. + # newTab - Optional, boolean. Whether to open the result in a new tab. open: (options) -> unless @vomnibarElement? @vomnibarElement = document.createElement "iframe" diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 2e51082a..81401623 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -1,3 +1,8 @@ +# +# This controls the contents of the Vomnibar iframe. We use an iframe to avoid changing the selection on the +# page (useful for bookmarklets), ensure that the Vomnibar style is unaffected by the page, and simplify key +# handling in vimium_frontend.coffee +# Vomnibar = vomnibarUI: null # the dialog instance for this window completers: {} diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index 72f54c9d..e6427e85 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -54,6 +54,9 @@
        +
        -- cgit v1.2.3 From c6531a8765932d39426dde6215b4d623460d3d56 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 31 Oct 2014 12:44:58 +0000 Subject: Reinstate .vomnibarIcon style. --- pages/vomnibar.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pages/vomnibar.css b/pages/vomnibar.css index 958445e6..18f1185f 100644 --- a/pages/vomnibar.css +++ b/pages/vomnibar.css @@ -85,6 +85,13 @@ padding: 2px 0; } +#vomnibar li .vomnibarIcon { + background-position-y: center; + background-size: 16px; + background-repeat: no-repeat; + padding-left: 20px; +} + #vomnibar li .vomnibarSource { color: #777; margin-right: 4px; -- cgit v1.2.3 From 3e175bcf39d4794937ca493f27c06bb63d739c63 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 31 Oct 2014 13:01:40 +0000 Subject: URLdecode string options for vomnibar. --- pages/vomnibar.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 81401623..ece0d756 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -271,6 +271,7 @@ initializeOnDomReady = -> .map((option) -> [name, value] = option.split "=" options[name] = value + options[name] = unescape(value) if value ) # Set boolean options -- cgit v1.2.3 From 390a9cb85e2a97fbd9d2b1a400deee4b12394337 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 31 Oct 2014 15:02:37 +0000 Subject: Fix vomnibar/iframe tests. --- pages/vomnibar.coffee | 75 +++++++++++++----------------------- tests/dom_tests/dom_tests.html | 5 ++- tests/dom_tests/vomnibar_test.coffee | 2 +- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index ece0d756..8b2661fe 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -5,6 +5,7 @@ # Vomnibar = vomnibarUI: null # the dialog instance for this window + getUI: -> @vomnibarUI completers: {} getCompleter: (name) -> @@ -15,7 +16,31 @@ Vomnibar = # # Activate the Vomnibox. # - activateWithCompleter: (options) -> + activate: (params = "") -> + options = + completer: "omni" + query: null + frameId: -1 + + booleanOptions = ["selectFirst", "newTab"] + + # Convert options/params in URL to options object. + params + .split(/[\?&]/) + .map((option) -> + [name, value] = option.split "=" + options[name] = value + options[name] = unescape(value) if value + ) + + # Set boolean options. + for option in booleanOptions + options[option] = option of options and options[option] != "false" + + options.refreshInterval = switch options.completer + when "omni" then 100 + else 0 + completer = @getCompleter(options.completer) @vomnibarUI ?= new VomnibarUI() completer.refresh() @@ -29,28 +54,6 @@ Vomnibar = @vomnibarUI.setQuery(options.query) @vomnibarUI.update() - activate: -> @activateWithCompleter {completer:"omni"} - activateInNewTab: -> @activateWithCompleter { - completer: "omni" - selectFirst: false - newTab: true - } - activateTabSelection: -> @activateWithCompleter { - completer: "tabs" - selectFirst: true - } - activateBookmarks: -> @activateWithCompleter { - completer: "bookmarks" - selectFirst: true - } - activateBookmarksInNewTab: -> @activateWithCompleter { - completer: "bookmarks" - selectFirst: true - newTab: true - } - getUI: -> @vomnibarUI - - class VomnibarUI constructor: -> @refreshInterval = 0 @@ -258,31 +261,7 @@ extend BackgroundCompleter, switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) initializeOnDomReady = -> - options = - completer: "omni" - query: null - frameId: -1 - - booleanOptions = ["selectFirst", "newTab"] - - # Convert options in URL to options object - document.location.search - .split(/[\?&]/) - .map((option) -> - [name, value] = option.split "=" - options[name] = value - options[name] = unescape(value) if value - ) - - # Set boolean options - for option in booleanOptions - options[option] = option of options and options[option] != "false" - - options.refreshInterval = switch options.completer - when "omni" then 100 - else 0 - - Vomnibar.activateWithCompleter options + Vomnibar.activate document.location.search window.addEventListener "DOMContentLoaded", initializeOnDomReady diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index e6427e85..6378807f 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -35,7 +35,10 @@ - + diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee index dc2a849f..f7241552 100644 --- a/tests/dom_tests/vomnibar_test.coffee +++ b/tests/dom_tests/vomnibar_test.coffee @@ -28,7 +28,7 @@ context "Keep selection within bounds", assert.equal -1, ui.selection should "set selection to position 0 for bookmark completion if possible", -> - Vomnibar.activateBookmarks() + Vomnibar.activate "completer=bookmark&selectFirst" ui = Vomnibar.vomnibarUI @completions = [] -- cgit v1.2.3 From d42913f75443df565d33ef5c565cd80c31714a60 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 2 Nov 2014 13:35:15 +0000 Subject: Minor tweak for clarity. --- pages/vomnibar.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 8b2661fe..59bf8266 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -29,8 +29,7 @@ Vomnibar = .split(/[\?&]/) .map((option) -> [name, value] = option.split "=" - options[name] = value - options[name] = unescape(value) if value + options[name] = if value? then unescape(value) else true ) # Set boolean options. -- cgit v1.2.3 From dcc7e1ff5ae28b700e12ecf75d403001fc9c2152 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sat, 6 Dec 2014 21:58:04 +0000 Subject: Only consider fullscreen elements' children for link hints --- content_scripts/link_hints.coffee | 7 ++++++- lib/dom_utils.coffee | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 24bd7126..80d091dd 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -141,7 +141,12 @@ LinkHints = # of digits needed to enumerate all of the links on screen. # getVisibleClickableElements: -> - resultSet = DomUtils.evaluateXPath(@clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) + # If we are fullscreen, only consider elements contained in the fullscreen element. + resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE + resultSet = if document.webkitIsFullScreen + DomUtils.evaluateXPath(@clickableElementsXPath, resultType, document.webkitFullscreenElement) + else + DomUtils.evaluateXPath(@clickableElementsXPath, resultType) visibleElements = [] diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 21018049..5f316c08 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -33,13 +33,13 @@ DomUtils = makeXPath: (elementArray) -> xpath = [] for element in elementArray - xpath.push("//" + element, "//xhtml:" + element) + xpath.push(".//" + element, ".//xhtml:" + element) xpath.join(" | ") - evaluateXPath: (xpath, resultType) -> + evaluateXPath: (xpath, resultType, contextNode = document.documentElement) -> namespaceResolver = (namespace) -> if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null - document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null) + document.evaluate(xpath, contextNode, namespaceResolver, resultType, null) # # Returns the first visible clientRect of an element if it exists. Otherwise it returns null. -- cgit v1.2.3 From 1c33590eb994403ec8520c1c02bfb4fe63831745 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 8 Dec 2014 01:15:06 +0000 Subject: Detect port disconnects (ie. extension is disabled) and disable frontend --- content_scripts/vimium_frontend.coffee | 17 +++++++++++++++++ tests/dom_tests/chrome.coffee | 3 +++ 2 files changed, 20 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 469afe71..f7bd805d 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -57,6 +57,16 @@ settings = @port = chrome.runtime.connect({ name: "settings" }) @port.onMessage.addListener(@receiveMessage) + # If the port is closed, the background page has gone away (since we never close it ourselves). Stub the + # settings object so we don't keep trying to connect to the extension even though it's gone away. + @port.onDisconnect.addListener => + @port = null + _get = @get # @get doesn't depend on @port, so we can continue to support it to try and reduce errors. + for own property, value of this + @[property] = (->) if "function" == typeof value + @get = _get + + get: (key) -> @values[key] set: (key, value) -> @@ -109,6 +119,13 @@ initializePreDomReady = -> # Send the key to the key handler in the background page. keyPort = chrome.runtime.connect({ name: "keyDown" }) + # If the port is closed, the background page has gone away (since we never close it ourselves). Disable all + # our event listeners, and stub out chrome.runtime.sendMessage/connect (to prevent errors). + # TODO(mrmr1993): Do some actual cleanup to free resources, hide UI, etc. + keyPort.onDisconnect.addListener -> + isEnabledForUrl = false + chrome.runtime.sendMessage = -> + chrome.runtime.connect = -> requestHandlers = hideUpgradeNotification: -> HUD.hideUpgradeNotification() diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee index 7f99e27f..15ed4339 100644 --- a/tests/dom_tests/chrome.coffee +++ b/tests/dom_tests/chrome.coffee @@ -10,6 +10,9 @@ root.chrome = { onMessage: { addListener: -> } + onDisconnect: { + addListener: -> + } postMessage: -> } onMessage: { -- cgit v1.2.3 From fa06a27b906a83c01ded4e0fe3b96e8e51a1a52d Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sat, 13 Dec 2014 17:18:52 +0000 Subject: Simplify settings port cleanup logic --- content_scripts/vimium_frontend.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index f7bd805d..62247c5c 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -61,10 +61,9 @@ settings = # settings object so we don't keep trying to connect to the extension even though it's gone away. @port.onDisconnect.addListener => @port = null - _get = @get # @get doesn't depend on @port, so we can continue to support it to try and reduce errors. for own property, value of this - @[property] = (->) if "function" == typeof value - @get = _get + # @get doesn't depend on @port, so we can continue to support it to try and reduce errors. + @[property] = (->) if "function" == typeof value and property != "get" get: (key) -> @values[key] -- cgit v1.2.3 From 82413203d258fafc0fc8da0d9b2daf760b46017b Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 19 May 2014 15:40:40 +0100 Subject: Implement cursor hiding on scroll This begins to address #662, by hiding the cursor when the page is scrolled to prevent a distracting cursor and accidentally triggering hover effects. --- content_scripts/vimium_frontend.coffee | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 469afe71..98e9a172 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -183,6 +183,7 @@ initializeOnDomReady = -> # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) + CursorHider.init() registerFrame = -> # Don't register frameset containers; focusing them is no use. @@ -1067,6 +1068,59 @@ Tween = value = (elapsed / state.duration) * (state.to - state.from) + state.from state.onUpdate(value) +CursorHider = + # + # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible + # + cursorHidden: false + hoverBlockElement: undefined + + hideCursor: -> + unless @cursorHidden + @hoverBlockElement.style.display = "block" + @cursorHidden = true + + showCursor: -> + if @cursorHidden + @hoverBlockElement.style.display = "none" + @cursorHidden = false + + onMouseMove: (event) -> + CursorHider.showCursor() + + onScroll: -> + CursorHider.hideCursor() + + # Ignore next mousemove, caused by the scrolling, so the mouse doesn't re-show straight away. + window.removeEventListener "mousemove", CursorHider.onMouseMove + window.addEventListener "mousemove", -> + window.addEventListener "mousemove", CursorHider.onMouseMove + window.removeEventListener "mousemove", arguments.callee + + init: -> + # cover the element entirely by a div with cursor: none + @hoverBlockElement = document.createElement("div") + @hoverBlockElement.style.position = "fixed" + @hoverBlockElement.style.width = "104%" + @hoverBlockElement.style.height = "104%" + @hoverBlockElement.style.zIndex = "2147483647" # Maximum value of z-index + @hoverBlockElement.style.left = "0px" + @hoverBlockElement.style.top = "0px" + @hoverBlockElement.style.opacity = "0.0" + @hoverBlockElement.style.display = "none" + @hoverBlockElement.style.cursor = "none" + @hoverBlockElement.style.border = "none" + @hoverBlockElement.style.margin = "-2% -2% -2% -2%" + document.body.appendChild(@hoverBlockElement) + + window.addEventListener "mousemove", @onMouseMove + window.addEventListener "scroll", @onScroll + + deinit: -> + window.removeEventListener "mousemove", @onMouseMove + window.removeEventListener "scroll", @onScroll + @showCursor() + initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) window.addEventListener("unload", unregisterFrame) -- cgit v1.2.3 From 70dcf4131a83a51e503d929438ba9eb887cbf0bd Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 14 Dec 2014 09:58:30 +0000 Subject: Use cursor: none and pointer-events: none to hide the cursor on scroll --- content_scripts/vimium_frontend.coffee | 40 ++++++++++------------------------ 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 98e9a172..f6a60c9c 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1072,23 +1072,15 @@ CursorHider = # # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible # - cursorHidden: false - hoverBlockElement: undefined + cursorHideStyle: null + cursorHideTimeout: 5000 + showCursor: -> @cursorHideStyle.remove() hideCursor: -> - unless @cursorHidden - @hoverBlockElement.style.display = "block" - @cursorHidden = true + document.head.appendChild @cursorHideStyle unless @cursorHideStyle.parentElement - showCursor: -> - if @cursorHidden - @hoverBlockElement.style.display = "none" - @cursorHidden = false - - onMouseMove: (event) -> - CursorHider.showCursor() - - onScroll: -> + onMouseMove: (event) -> CursorHider.showCursor() + onScroll: (event) -> CursorHider.hideCursor() # Ignore next mousemove, caused by the scrolling, so the mouse doesn't re-show straight away. @@ -1098,21 +1090,11 @@ CursorHider = window.removeEventListener "mousemove", arguments.callee init: -> - # cover the element entirely by a div with cursor: none - @hoverBlockElement = document.createElement("div") - @hoverBlockElement.style.position = "fixed" - @hoverBlockElement.style.width = "104%" - @hoverBlockElement.style.height = "104%" - @hoverBlockElement.style.zIndex = "2147483647" # Maximum value of z-index - @hoverBlockElement.style.left = "0px" - @hoverBlockElement.style.top = "0px" - @hoverBlockElement.style.opacity = "0.0" - @hoverBlockElement.style.display = "none" - @hoverBlockElement.style.cursor = "none" - @hoverBlockElement.style.border = "none" - @hoverBlockElement.style.margin = "-2% -2% -2% -2%" - document.body.appendChild(@hoverBlockElement) - + @cursorHideStyle = document.createElement("style") + @cursorHideStyle.innerHTML = """ + body * {pointer-events: none !important; cursor: none !important;} + body {cursor: none !important;} + """ window.addEventListener "mousemove", @onMouseMove window.addEventListener "scroll", @onScroll -- cgit v1.2.3 From 9b9cee761f4c718f08dec92c46027a7effe39991 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 14 Dec 2014 11:19:36 +0000 Subject: Remove unused code in CursorHider --- content_scripts/vimium_frontend.coffee | 6 ------ 1 file changed, 6 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index f6a60c9c..23e320c0 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1073,7 +1073,6 @@ CursorHider = # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible # cursorHideStyle: null - cursorHideTimeout: 5000 showCursor: -> @cursorHideStyle.remove() hideCursor: -> @@ -1098,11 +1097,6 @@ CursorHider = window.addEventListener "mousemove", @onMouseMove window.addEventListener "scroll", @onScroll - deinit: -> - window.removeEventListener "mousemove", @onMouseMove - window.removeEventListener "scroll", @onScroll - @showCursor() - initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) window.addEventListener("unload", unregisterFrame) -- cgit v1.2.3 From aa190905bff04bd83438960779ce912b048b1f5f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 14 Dec 2014 11:51:42 +0000 Subject: Use a boolean to track whether we are scrolling in CursorHider --- content_scripts/vimium_frontend.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 23e320c0..30d76523 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1073,21 +1073,21 @@ CursorHider = # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible # cursorHideStyle: null + isScrolling: false showCursor: -> @cursorHideStyle.remove() hideCursor: -> document.head.appendChild @cursorHideStyle unless @cursorHideStyle.parentElement - onMouseMove: (event) -> CursorHider.showCursor() + onMouseMove: (event) -> + if CursorHider.isScrolling # This event was caused by scrolling, don't show the cursor. + CursorHider.isScrolling = false + else + CursorHider.showCursor() onScroll: (event) -> + CursorHider.isScrolling = true CursorHider.hideCursor() - # Ignore next mousemove, caused by the scrolling, so the mouse doesn't re-show straight away. - window.removeEventListener "mousemove", CursorHider.onMouseMove - window.addEventListener "mousemove", -> - window.addEventListener "mousemove", CursorHider.onMouseMove - window.removeEventListener "mousemove", arguments.callee - init: -> @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ -- cgit v1.2.3 From 69c117102eb419baa649cccc770bffea5177b1e1 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 14 Dec 2014 11:57:19 +0000 Subject: Check if cursorHideStyle is in the document before remove()-ing it --- content_scripts/vimium_frontend.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 30d76523..639a4041 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1075,7 +1075,8 @@ CursorHider = cursorHideStyle: null isScrolling: false - showCursor: -> @cursorHideStyle.remove() + showCursor: -> + @cursorHideStyle.remove() if @cursorHideStyle.parentElement hideCursor: -> document.head.appendChild @cursorHideStyle unless @cursorHideStyle.parentElement -- cgit v1.2.3 From 9c677fc91cd2641c709bb1afe03466c92b0ee0ea Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 14 Dec 2014 15:36:15 +0000 Subject: Minor refactoring of CursorHider. --- content_scripts/vimium_frontend.coffee | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index aeb74dcd..9478a7d5 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1089,23 +1089,20 @@ Tween = CursorHider = # # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible + # NOTE(smblott) onScroll and onMouseMove events come in pairs. # cursorHideStyle: null isScrolling: false - showCursor: -> - @cursorHideStyle.remove() if @cursorHideStyle.parentElement - hideCursor: -> - document.head.appendChild @cursorHideStyle unless @cursorHideStyle.parentElement - - onMouseMove: (event) -> - if CursorHider.isScrolling # This event was caused by scrolling, don't show the cursor. - CursorHider.isScrolling = false - else - CursorHider.showCursor() onScroll: (event) -> CursorHider.isScrolling = true - CursorHider.hideCursor() + unless CursorHider.cursorHideStyle.parentElement + document.head.appendChild CursorHider.cursorHideStyle + + onMouseMove: (event) -> + if CursorHider.cursorHideStyle.parentElement and not CursorHider.isScrolling + CursorHider.cursorHideStyle.remove() + CursorHider.isScrolling = false init: -> @cursorHideStyle = document.createElement("style") -- cgit v1.2.3 From 886e94e5c1563483702208675c5d12735f007938 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 14 Dec 2014 15:58:22 +0000 Subject: FRAMESET frames should not be registered. Somewhere along the road, this got goofed up. This reverts to the intended behaviour. --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 9478a7d5..fc65f6c0 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -205,7 +205,7 @@ initializeOnDomReady = -> registerFrame = -> # Don't register frameset containers; focusing them is no use. - if document.body.tagName != "FRAMESET" + if document.body.tagName.toLowerCase() != "frameset" chrome.runtime.sendMessage handler: "registerFrame" frameId: frameId -- cgit v1.2.3 From 4837af619b39e92b0eacd082867ba6b139875ba5 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 15 Dec 2014 00:07:35 +0000 Subject: Restrict evaluateXPath to the fullscreen element when fullscreen --- content_scripts/link_hints.coffee | 7 +------ lib/dom_utils.coffee | 8 +++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 80d091dd..24bd7126 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -141,12 +141,7 @@ LinkHints = # of digits needed to enumerate all of the links on screen. # getVisibleClickableElements: -> - # If we are fullscreen, only consider elements contained in the fullscreen element. - resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE - resultSet = if document.webkitIsFullScreen - DomUtils.evaluateXPath(@clickableElementsXPath, resultType, document.webkitFullscreenElement) - else - DomUtils.evaluateXPath(@clickableElementsXPath, resultType) + resultSet = DomUtils.evaluateXPath(@clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) visibleElements = [] diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 5f316c08..95427903 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -36,7 +36,13 @@ DomUtils = xpath.push(".//" + element, ".//xhtml:" + element) xpath.join(" | ") - evaluateXPath: (xpath, resultType, contextNode = document.documentElement) -> + # Evaluates an XPath on the whole document, or on the contents of the fullscreen element if an element is + # fullscreen. + evaluateXPath: (xpath, resultType) -> + contextNode = if document.webkitIsFullScreen + document.webkitFullscreenElement + else + document.documentElement namespaceResolver = (namespace) -> if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null document.evaluate(xpath, contextNode, namespaceResolver, resultType, null) -- cgit v1.2.3 From d287105f34a98bef4fbb57292274a16252a3e0e6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 15 Dec 2014 07:47:32 +0000 Subject: Minor cleanup in evaluateXPath. --- lib/dom_utils.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index c2bfad4c..2caaa00e 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -39,10 +39,8 @@ DomUtils = # Evaluates an XPath on the whole document, or on the contents of the fullscreen element if an element is # fullscreen. evaluateXPath: (xpath, resultType) -> - contextNode = if document.webkitIsFullScreen - document.webkitFullscreenElement - else - document.documentElement + contextNode = + if document.webkitIsFullScreen then document.webkitFullscreenElement else document.documentElement namespaceResolver = (namespace) -> if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null document.evaluate(xpath, contextNode, namespaceResolver, resultType, null) -- cgit v1.2.3 From bea41e9c695f3b3f8e921880b3d40b681ca387b0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 15 Dec 2014 12:51:15 +0000 Subject: Fix z-index to be consistent with vimium.css. The z-indexes in vimium.css were recently changed. Those changes didn't get pushed through into vomnibar.css when moving the vomnibar to an iframe. Fixed here. --- pages/vomnibar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/vomnibar.css b/pages/vomnibar.css index 18f1185f..c4bc0008 100644 --- a/pages/vomnibar.css +++ b/pages/vomnibar.css @@ -24,7 +24,7 @@ box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); border: 1px solid #aaa; /* One less than hint markers and the help dialog. */ - z-index: 99999996; + z-index: 2147483646; } #vomnibar input { -- cgit v1.2.3 From a199335790aec50cf3ed7cc27c5b407875c37107 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 10:15:54 +0000 Subject: Use the DOM rather than XPath to detect clickable elements --- content_scripts/link_hints.coffee | 16 ++-------------- lib/dom_utils.coffee | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 24bd7126..2ffe818f 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -36,17 +36,6 @@ LinkHints = # init: -> - # - # Generate an XPath describing what a clickable element is. - # The final expression will be something like "//button | //xhtml:button | ..." - # We use translate() instead of lower-case() because Chrome only supports XPath 1.0. - # - clickableElementsXPath: DomUtils.makeXPath( - ["a", "area[@href]", "textarea", "button", "select", - "input[not(@type='hidden' or @disabled or @readonly)]", - "*[@onclick or @tabindex or @role='link' or @role='button' or contains(@class, 'button') or " + - "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]) - # We need this as a top-level function because our command system doesn't yet support arguments. activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_BG_TAB) activateModeToOpenInNewForegroundTab: -> @activateMode(OPEN_IN_NEW_FG_TAB) @@ -141,13 +130,12 @@ LinkHints = # of digits needed to enumerate all of the links on screen. # getVisibleClickableElements: -> - resultSet = DomUtils.evaluateXPath(@clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) + resultSet = DomUtils.getClickableElements() visibleElements = [] # Find all visible clickable elements. - for i in [0...resultSet.snapshotLength] by 1 - element = resultSet.snapshotItem(i) + for element in resultSet clientRect = DomUtils.getVisibleClientRect(element, clientRect) if (clientRect != null) visibleElements.push({element: element, rect: clientRect}) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index a0ac0bd3..26fa9b81 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -41,6 +41,42 @@ DomUtils = if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null) + # + # Returns all the clickable element children of contextNode. This also can include contextNode itself. + # + getClickableElements: (contextNode = document.documentElement) -> + elements = Array::slice.call(contextNode?.getElementsByTagName "*") + elements.unshift contextNode # Check the contextNode as well. + clickableElements = [] + for element in elements + isClickable = false + tagName = element.tagName.toLowerCase() + isClickable = (-> + if element.hasAttribute "onclick" + true + else if element.hasAttribute "tabindex" + true + else if element.getAttribute "role" in ["button", "link"] + true + else if element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 + true + else if element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"] + true + else if tagName == "a" + true + else if tagName == "area" + element.hasAttribute "href" + else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" + not (element.disabled or element.hasAttribute "readonly") + else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or + tagName in ["button", "select"] + not element.disabled + else + false + )() + clickableElements.push element if isClickable + clickableElements + # # Returns the first visible clientRect of an element if it exists. Otherwise it returns null. # -- cgit v1.2.3 From c80ad2c367f873f2b2547b60cebe49715a85ffe4 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 10:40:02 +0000 Subject: Treat area elements as being at the point of their img element --- lib/dom_utils.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 26fa9b81..152a378e 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -64,8 +64,13 @@ DomUtils = true else if tagName == "a" true - else if tagName == "area" - element.hasAttribute "href" + else if tagName == "img" + mapName = element.getAttribute "usemap" + if mapName + map = document.querySelector(mapName.replace /^#/, "") + areas = Array::slice.call(map.getElementsByTagName "area") + elements.concat areas + false else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" not (element.disabled or element.hasAttribute "readonly") else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or -- cgit v1.2.3 From 8c8ec835d673f0ec1cce242cf26cca077c845064 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 11:08:07 +0000 Subject: Use element.readOnly instead of getAttribute "readonly" --- lib/dom_utils.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 152a378e..46bf3639 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -72,7 +72,7 @@ DomUtils = elements.concat areas false else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" - not (element.disabled or element.hasAttribute "readonly") + not (element.disabled or element.readOnly) else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or tagName in ["button", "select"] not element.disabled -- cgit v1.2.3 From c7e2f1cdef2d5e99761d7bb8ecbad91f89de6958 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 11:12:58 +0000 Subject: Inline DomUtils.getClickableElements --- content_scripts/link_hints.coffee | 36 +++++++++++++++++++++++++++++++++- lib/dom_utils.coffee | 41 --------------------------------------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 2ffe818f..fe36cee0 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -130,7 +130,41 @@ LinkHints = # of digits needed to enumerate all of the links on screen. # getVisibleClickableElements: -> - resultSet = DomUtils.getClickableElements() + elements = Array::slice.call(document.documentElement.getElementsByTagName "*") + resultSet = [] + + for element in elements + isClickable = false + tagName = element.tagName.toLowerCase() + isClickable = (-> + if element.hasAttribute "onclick" + true + else if element.hasAttribute "tabindex" + true + else if element.getAttribute "role" in ["button", "link"] + true + else if element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 + true + else if element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"] + true + else if tagName == "a" + true + else if tagName == "img" + mapName = element.getAttribute "usemap" + if mapName + map = document.querySelector(mapName.replace /^#/, "") + areas = Array::slice.call(map.getElementsByTagName "area") + resultSet.concat areas + false + else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" + not (element.disabled or element.readOnly) + else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or + tagName in ["button", "select"] + not element.disabled + else + false + )() + resultSet.push element if isClickable visibleElements = [] diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 46bf3639..a0ac0bd3 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -41,47 +41,6 @@ DomUtils = if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null) - # - # Returns all the clickable element children of contextNode. This also can include contextNode itself. - # - getClickableElements: (contextNode = document.documentElement) -> - elements = Array::slice.call(contextNode?.getElementsByTagName "*") - elements.unshift contextNode # Check the contextNode as well. - clickableElements = [] - for element in elements - isClickable = false - tagName = element.tagName.toLowerCase() - isClickable = (-> - if element.hasAttribute "onclick" - true - else if element.hasAttribute "tabindex" - true - else if element.getAttribute "role" in ["button", "link"] - true - else if element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 - true - else if element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"] - true - else if tagName == "a" - true - else if tagName == "img" - mapName = element.getAttribute "usemap" - if mapName - map = document.querySelector(mapName.replace /^#/, "") - areas = Array::slice.call(map.getElementsByTagName "area") - elements.concat areas - false - else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" - not (element.disabled or element.readOnly) - else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or - tagName in ["button", "select"] - not element.disabled - else - false - )() - clickableElements.push element if isClickable - clickableElements - # # Returns the first visible clientRect of an element if it exists. Otherwise it returns null. # -- cgit v1.2.3 From 28c275ae4128f0a2907c7ad3d27cedc81efe129a Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 11:41:10 +0000 Subject: Simplify finding clickable elements --- content_scripts/link_hints.coffee | 58 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index fe36cee0..18e9741d 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -134,37 +134,35 @@ LinkHints = resultSet = [] for element in elements - isClickable = false tagName = element.tagName.toLowerCase() - isClickable = (-> - if element.hasAttribute "onclick" - true - else if element.hasAttribute "tabindex" - true - else if element.getAttribute "role" in ["button", "link"] - true - else if element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 - true - else if element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"] - true - else if tagName == "a" - true - else if tagName == "img" - mapName = element.getAttribute "usemap" - if mapName - map = document.querySelector(mapName.replace /^#/, "") - areas = Array::slice.call(map.getElementsByTagName "area") - resultSet.concat areas - false - else if (tagName == "input" and DomUtils.isSelectable element) or tagName == "textarea" - not (element.disabled or element.readOnly) - else if (tagName == "input" and element.getAttribute("type")?.toLowerCase() != "hidden") or - tagName in ["button", "select"] - not element.disabled - else - false - )() - resultSet.push element if isClickable + + # Insert area elements that provide click functionality to an img. + if tagName == "img" + mapName = element.getAttribute "usemap" + if mapName + mapName = mapName.replace(/^#/, "").replace("\"", "\\\"") + map = document.querySelector "map[name=\"#{mapName}\"]" + areas = if map then Array::slice.call(map.getElementsByTagName "area") else [] + resultSet = resultSet.concat areas + + # Check for attributes that make an element clickable regardless of its tagName. + if (element.hasAttribute "onclick" or + element.hasAttribute "tabindex" or + element.getAttribute "role" in ["button", "link"] or + element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or + element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) + resultSet.push element + continue + + switch tagName + when "a" + resultSet.push element + when "textarea", "input" + unless (tagName == "input" and element.getAttribute("type")?.toLowerCase() == "hidden") or + element.disabled or (element.readOnly and DomUtils.isSelectable element) + resultSet.push element + when "button", "select" + resultSet.push element unless element.disabled visibleElements = [] -- cgit v1.2.3 From 5f9290693ab0f35c46cea6cea0a9f5c06b4ee0ad Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 12:27:40 +0000 Subject: Combine rectangle calculation and clickable element detection --- content_scripts/link_hints.coffee | 51 ++++++++++++--------------------------- lib/dom_utils.coffee | 28 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 18e9741d..dd359a70 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -131,19 +131,23 @@ LinkHints = # getVisibleClickableElements: -> elements = Array::slice.call(document.documentElement.getElementsByTagName "*") - resultSet = [] + visibleElements = [] for element in elements tagName = element.tagName.toLowerCase() + isClickable = false # Insert area elements that provide click functionality to an img. if tagName == "img" mapName = element.getAttribute "usemap" if mapName + imgClientRects = element.getClientRects() mapName = mapName.replace(/^#/, "").replace("\"", "\\\"") map = document.querySelector "map[name=\"#{mapName}\"]" - areas = if map then Array::slice.call(map.getElementsByTagName "area") else [] - resultSet = resultSet.concat areas + if map and imgClientRects.length > 0 + areas = map.getElementsByTagName "area" + areaRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas + visibleElements = visibleElements.concat areaRects # Check for attributes that make an element clickable regardless of its tagName. if (element.hasAttribute "onclick" or @@ -151,46 +155,23 @@ LinkHints = element.getAttribute "role" in ["button", "link"] or element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) - resultSet.push element - continue + isClickable = true + # Check for tagNames which are natively clickable. switch tagName when "a" - resultSet.push element + isClickable = true when "textarea", "input" unless (tagName == "input" and element.getAttribute("type")?.toLowerCase() == "hidden") or element.disabled or (element.readOnly and DomUtils.isSelectable element) - resultSet.push element + isClickable = true when "button", "select" - resultSet.push element unless element.disabled - - visibleElements = [] + isClickable = not element.disabled - # Find all visible clickable elements. - for element in resultSet - clientRect = DomUtils.getVisibleClientRect(element, clientRect) - if (clientRect != null) - visibleElements.push({element: element, rect: clientRect}) - - if (element.localName == "area") - map = element.parentElement - continue unless map - img = document.querySelector("img[usemap='#" + map.getAttribute("name") + "']") - continue unless img - imgClientRects = img.getClientRects() - continue if (imgClientRects.length == 0) - c = element.coords.split(/,/) - coords = [parseInt(c[0], 10), parseInt(c[1], 10), parseInt(c[2], 10), parseInt(c[3], 10)] - rect = { - top: imgClientRects[0].top + coords[1], - left: imgClientRects[0].left + coords[0], - right: imgClientRects[0].left + coords[2], - bottom: imgClientRects[0].top + coords[3], - width: coords[2] - coords[0], - height: coords[3] - coords[1] - } - - visibleElements.push({element: element, rect: rect}) + continue unless isClickable # If the element isn't clickable, do nothing. + clientRect = DomUtils.getVisibleClientRect element + if clientRect != null + visibleElements.push {element: element, rect: clientRect} visibleElements diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index a0ac0bd3..3d7e805f 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -90,6 +90,34 @@ DomUtils = return childClientRect null + getClientRectsForAreas: (imgClientRect, areas) -> + rects = [] + for area in areas + coords = area.coords.split(",").map((coord) -> parseInt(coord, 10)) + shape = area.shape.toLowerCase() + if shape == "rect" + [x1, y1, x2, y2] = coords + else if shape == "circle" + [x, y, r] = coords + x1 = x - r + x2 = x + r + y1 = y - r + y2 = y + r + else # For polygons and unknown shapes, don't return a rectangle. + # TODO(mrmr1993): revisit this. + continue + + rect = + top: imgClientRect.top + y1 + left: imgClientRect.left + x1 + right: imgClientRect.left + x2 + bottom: imgClientRect.top + y2 + width: x2 - x1 + height: y2 - y1 + + rects.push {element: area, rect: rect} unless isNaN rect.top + rects + # # Selectable means that we should use the simulateSelect method to activate the element instead of a click. # -- cgit v1.2.3 From 62686e83d690919f00afe1ac7f5955cecb1d2b2f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 12:31:09 +0000 Subject: Try to make image map rectangles work better --- lib/dom_utils.coffee | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 3d7e805f..842dda0f 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -95,17 +95,18 @@ DomUtils = for area in areas coords = area.coords.split(",").map((coord) -> parseInt(coord, 10)) shape = area.shape.toLowerCase() - if shape == "rect" + if shape == "rect" or coords.length == 4 [x1, y1, x2, y2] = coords - else if shape == "circle" + else if shape == "circle" or coords.length == 3 [x, y, r] = coords x1 = x - r x2 = x + r y1 = y - r y2 = y + r - else # For polygons and unknown shapes, don't return a rectangle. - # TODO(mrmr1993): revisit this. - continue + else + # Just consider the rectangle surrounding the first two points in a polygon. It's possible to do + # something more sophisticated, but likely not worth the effort. + [x1, y1, x2, y2] = coords rect = top: imgClientRect.top + y1 -- cgit v1.2.3 From c2ab9aaa27b5fbf1f065743772c6f04dd3c5f39d Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 12:39:22 +0000 Subject: Don't show link hints for offscreen image maps --- lib/dom_utils.coffee | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 842dda0f..8ade58bb 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -52,18 +52,9 @@ DomUtils = } for clientRect in element.getClientRects()) for clientRect in clientRects - if (clientRect.top < 0) - clientRect.height += clientRect.top - clientRect.top = 0 + clientRect = @cropRectToVisible clientRect - if (clientRect.left < 0) - clientRect.width += clientRect.left - clientRect.left = 0 - - if (clientRect.top >= window.innerHeight - 4 || clientRect.left >= window.innerWidth - 4) - continue - - if (clientRect.width < 3 || clientRect.height < 3) + if (!clientRect || clientRect.width < 3 || clientRect.height < 3) continue # eliminate invisible elements (see test_harnesses/visibility_test.html) @@ -90,6 +81,21 @@ DomUtils = return childClientRect null + cropRectToVisible: (rect) -> + if (rect.top < 0) + rect.height += rect.top + rect.top = 0 + + if (rect.left < 0) + rect.width += rect.left + rect.left = 0 + + if (rect.top >= window.innerHeight - 4 || rect.left >= window.innerWidth - 4) + null + else + rect + + getClientRectsForAreas: (imgClientRect, areas) -> rects = [] for area in areas @@ -108,7 +114,7 @@ DomUtils = # something more sophisticated, but likely not worth the effort. [x1, y1, x2, y2] = coords - rect = + rect = @cropRectToVisible top: imgClientRect.top + y1 left: imgClientRect.left + x1 right: imgClientRect.left + x2 @@ -116,7 +122,7 @@ DomUtils = width: x2 - x1 height: y2 - y1 - rects.push {element: area, rect: rect} unless isNaN rect.top + rects.push {element: area, rect: rect} unless not rect or isNaN rect.top rects # -- cgit v1.2.3 From 82beb23ce138505f0358ec8e15a56d20db6846dd Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 12:42:31 +0000 Subject: Remove redundant array conversion --- content_scripts/link_hints.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index dd359a70..4b039935 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -130,7 +130,7 @@ LinkHints = # of digits needed to enumerate all of the links on screen. # getVisibleClickableElements: -> - elements = Array::slice.call(document.documentElement.getElementsByTagName "*") + elements = document.documentElement.getElementsByTagName "*" visibleElements = [] for element in elements -- cgit v1.2.3 From 833942ae06f680bc1949a7bced4719b707950568 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 12:47:38 +0000 Subject: Stop ignoring clickable opacity: none; elements Some websites (notably Facebook) use `opacity: none;` to show an image in the place of a less-customisable element (eg. an ``). To not show link hints for such transparent elements is confusing and often the wrong thing to do. --- lib/dom_utils.coffee | 3 +-- tests/dom_tests/dom_utils_test.coffee | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 8ade58bb..1e2cc812 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -60,8 +60,7 @@ DomUtils = # eliminate invisible elements (see test_harnesses/visibility_test.html) computedStyle = window.getComputedStyle(element, null) if (computedStyle.getPropertyValue('visibility') != 'visible' || - computedStyle.getPropertyValue('display') == 'none' || - computedStyle.getPropertyValue('opacity') == '0') + computedStyle.getPropertyValue('display') == 'none') continue return clientRect diff --git a/tests/dom_tests/dom_utils_test.coffee b/tests/dom_tests/dom_utils_test.coffee index 130a3014..ad8bde3c 100644 --- a/tests/dom_tests/dom_utils_test.coffee +++ b/tests/dom_tests/dom_utils_test.coffee @@ -50,12 +50,6 @@ context "Check visibility", assert.isTrue (DomUtils.getVisibleClientRect document.getElementById 'foo') != null assert.isTrue (DomUtils.getVisibleClientRect document.getElementById 'bar') != null - should "detect opacity:0 links as hidden", -> - document.getElementById("test-div").innerHTML = """ - test - """ - assert.equal null, DomUtils.getVisibleClientRect document.getElementById 'foo' - should "detect links that contain only floated / absolutely-positioned divs as visible", -> document.getElementById("test-div").innerHTML = """ -- cgit v1.2.3 From 158b3f09fd222b0e93510dc17521833de73bcf88 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 13:49:29 +0000 Subject: Unify two loops into one --- lib/dom_utils.coffee | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 1e2cc812..aaa93923 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -51,20 +51,6 @@ DomUtils = width: clientRect.width, height: clientRect.height } for clientRect in element.getClientRects()) - for clientRect in clientRects - clientRect = @cropRectToVisible clientRect - - if (!clientRect || clientRect.width < 3 || clientRect.height < 3) - continue - - # eliminate invisible elements (see test_harnesses/visibility_test.html) - computedStyle = window.getComputedStyle(element, null) - if (computedStyle.getPropertyValue('visibility') != 'visible' || - computedStyle.getPropertyValue('display') == 'none') - continue - - return clientRect - for clientRect in clientRects # If the link has zero dimensions, it may be wrapping visible # but floated elements. Check for this. @@ -78,6 +64,21 @@ DomUtils = childClientRect = @getVisibleClientRect(child) continue if (childClientRect == null) return childClientRect + + else + clientRect = @cropRectToVisible clientRect + + if (!clientRect || clientRect.width < 3 || clientRect.height < 3) + continue + + # eliminate invisible elements (see test_harnesses/visibility_test.html) + computedStyle = window.getComputedStyle(element, null) + if (computedStyle.getPropertyValue('visibility') != 'visible' || + computedStyle.getPropertyValue('display') == 'none') + continue + + return clientRect + null cropRectToVisible: (rect) -> -- cgit v1.2.3 From 932652086c9ad601c92ef4aae250f9e0b6ad51a6 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 14:51:25 +0000 Subject: Ensure we only init CursorHider once --- content_scripts/vimium_frontend.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e4680ff7..78014490 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1124,6 +1124,7 @@ CursorHider = CursorHider.isScrolling = false init: -> + return unless @cursorHideStyle? @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ body * {pointer-events: none !important; cursor: none !important;} -- cgit v1.2.3 From 6ceb3bcc612cd4478187610cf61a82a8f95534ac Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 17 Dec 2014 15:37:57 +0000 Subject: Revert 932652086c9ad601c92ef4aae250f9e0b6ad51a6. --- content_scripts/vimium_frontend.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 78014490..e4680ff7 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1124,7 +1124,6 @@ CursorHider = CursorHider.isScrolling = false init: -> - return unless @cursorHideStyle? @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ body * {pointer-events: none !important; cursor: none !important;} -- cgit v1.2.3 From 3b25fd52431130aa5c3e9359075574458c307ef2 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 17 Dec 2014 15:49:57 +0000 Subject: Disable CursorHider pending fix for #1345. --- content_scripts/vimium_frontend.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e4680ff7..6dd244de 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1124,6 +1124,8 @@ CursorHider = CursorHider.isScrolling = false init: -> + # NOTE(smblott) CursorHider is currently disabled pending a fix for #1345. + return @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ body * {pointer-events: none !important; cursor: none !important;} -- cgit v1.2.3 From c972978b43b943a1ad8709992d080bedbbe12ae2 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 17 Dec 2014 16:44:47 +0000 Subject: Add comment re. Math.sign(). --- content_scripts/scroller.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 09470158..2f69fc7d 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -5,6 +5,10 @@ activatedElement = null # Return 0, -1 or 1: the sign of the argument. +# NOTE(smblott; 2014/12/17) We would like to use Math.sign(). However, according to this site +# (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign) Math.sign() was +# only introduced in Chrome 38. This caused problems in R1.48 for users with old Chrome installations. We +# can replace this with Math.sign() at some point. getSign = (val) -> if not val 0 -- cgit v1.2.3 From 2424b8ecc1eeb3b1728f7d4a30db77fccdc8fa05 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 14:39:52 +0000 Subject: Ensure cursor is hidden on scroll --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 6dd244de..fcf5dac1 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1129,7 +1129,7 @@ CursorHider = @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ body * {pointer-events: none !important; cursor: none !important;} - body {cursor: none !important;} + body, html {cursor: none !important;} """ window.addEventListener "mousemove", @onMouseMove window.addEventListener "scroll", @onScroll -- cgit v1.2.3 From 107cef3ecd0bdbcaba157b53cc3d20ff5e06b94f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 17 Dec 2014 20:21:33 +0000 Subject: Disable cursor hiding for Chrome versions < 39.0.2171.71 There is a bug for earlier versions of Chrome which leaves `pointer-events: none` stuck enabled for Google+ after a certain amount of scrolling. --- content_scripts/vimium_frontend.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index fcf5dac1..6f099e54 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1107,7 +1107,8 @@ Tween = CursorHider = # - # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible + # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible. + # Disabled for Chrome versions less than 39.0.2171.71 due to a browser error. # NOTE(smblott) onScroll and onMouseMove events come in pairs. # cursorHideStyle: null @@ -1124,8 +1125,10 @@ CursorHider = CursorHider.isScrolling = false init: -> - # NOTE(smblott) CursorHider is currently disabled pending a fix for #1345. - return + # Disable for Chrome versions less than 39.0.2171.71 due to a browser error. + chromeVersion = navigator.appVersion.match(/Chrome\/(.*?) /)?[1] || "" + return if 0 <= Utils.compareVersions "39.0.2171.71", chromeVersion + @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ body * {pointer-events: none !important; cursor: none !important;} -- cgit v1.2.3 From 855e9a4e19ab0926f5531c37272f00a715f45ed8 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 10:33:09 +0000 Subject: Remove overlapping rects from link hints --- content_scripts/link_hints.coffee | 30 +++++++++++++++++++- lib/dom_utils.coffee | 9 ++---- lib/utils.coffee | 60 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 4b039935..721070bb 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -173,7 +173,35 @@ LinkHints = if clientRect != null visibleElements.push {element: element, rect: clientRect} - visibleElements + # TODO(mrmr1993): Consider z-index. z-index affects behviour as follows: + # * The document has a local stacking context. + # * An element with z-index specified + # - sets its z-order position in the containing stacking context, and + # - creates a local stacking context containing its children. + # * An element (1) is shown above another element (2) if either + # - in the last stacking context which contains both an ancestor of (1) and an ancestor of (2), the + # ancestor of (1) has a higher z-index than the ancestor of (2); or + # - in the last stacking context which contains both an ancestor of (1) and an ancestor of (2), + # + the ancestors of (1) and (2) have equal z-index, and + # + the ancestor of (1) appears later in the DOM than the ancestor of (2). + # + # Remove rects from + nonOverlappingElements = [] + visibleElements = visibleElements.reverse() + while visibleElement = visibleElements.pop() + rects = [visibleElement.rect] + for {rect: negativeRect} in visibleElements + rects = Array::concat.apply [], (rects.map (rect) -> Utils.subtractRect rect, negativeRect) + if rects.length > 0 + nonOverlappingElements.push {element: visibleElement.element, rect: rects[0]} + else + # Every part of the element is covered by some other element, so just insert the whole element's + # rect. + # TODO(mrmr1993): This is probably the wrong thing to do, but we don't want to stop being able to + # click some elements that we could click before. + nonOverlappingElements.push visibleElement + + nonOverlappingElements # # Handles shift and esc keys. The other keys are passed to getMarkerMatcher().matchHintsByKey. diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index aaa93923..7e19a7fc 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -114,13 +114,8 @@ DomUtils = # something more sophisticated, but likely not worth the effort. [x1, y1, x2, y2] = coords - rect = @cropRectToVisible - top: imgClientRect.top + y1 - left: imgClientRect.left + x1 - right: imgClientRect.left + x2 - bottom: imgClientRect.top + y2 - width: x2 - x1 - height: y2 - y1 + rect = Utils.shiftRect (Utils.createRect x1, y1, x2, y2), imgClientRect.left, imgClientRect.top + rect = @cropRectToVisible rect rects.push {element: area, rect: rect} unless not rect or isNaN rect.top rects diff --git a/lib/utils.coffee b/lib/utils.coffee index b7f8731a..6cc45f32 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -136,6 +136,66 @@ Utils = # locale-sensitive uppercase detection hasUpperCase: (s) -> s.toLowerCase() != s + # Create a rect given the top left and bottom right corners. + createRect: (x1, y1, x2, y2) -> + bottom: y2 + top: y1 + left: x1 + right: x2 + width: x2 - x1 + height: y2 - y1 + + # Translate a rect by x horizontally and y vertically. + shiftRect: (rect, x, y) -> + bottom: rect.bottom + y + top: rect.top + y + left: rect.left + x + right: rect.right + x + width: rect.width + height: rect.height + + # Subtract rect2 from rect1, returning an array of rects which are in rect1 but not rect2. + subtractRect: (rect1, rect2_) -> + # Bound rect2 by rect1 + rect2 = {} + rect2 = @createRect( + Math.max(rect1.left, rect2_.left), + Math.max(rect1.top, rect2_.top), + Math.min(rect1.right, rect2_.right), + Math.min(rect1.bottom, rect2_.bottom) + ) + + # If bounding rect2 has made the width or height negative, rect1 does not contain rect2. + return [rect1] if rect2.width < 0 or rect2.height < 0 + + # + # All the possible rects, in the order + # +-+-+-+ + # |1|2|3| + # +-+-+-+ + # |4| |5| + # +-+-+-+ + # |6|7|8| + # +-+-+-+ + # where the outer rectangle is rect1 and the inner rectangle is rect 2. Note that the rects may be of + # width or height 0. + # + rects = [ + # Top row. + @createRect rect1.left, rect1.top, rect2.left, rect2.top + @createRect rect2.left, rect1.top, rect2.right, rect2.top + @createRect rect2.right, rect1.top, rect1.right, rect2.top + # Middle row. + @createRect rect1.left, rect2.top, rect2.left, rect2.bottom + @createRect rect2.right, rect2.top, rect1.right, rect2.bottom + # Bottom row. + @createRect rect1.left, rect2.bottom, rect2.left, rect1.bottom + @createRect rect2.left, rect2.bottom, rect2.right, rect1.bottom + @createRect rect2.right, rect2.bottom, rect1.right, rect1.bottom + ] + + rects.filter (rect) -> rect.height > 0 and rect.width > 0 + # 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. Function::curry = -> -- cgit v1.2.3 From 4e6513c47b6be2e771b2a8db6d5506d157368602 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 10:40:33 +0000 Subject: Add link hint support for jsaction event listeners This was adapted from PR #1316, commit 846a19efe51bfc639ae1ee84e18a5f2d3e12aaff --- content_scripts/link_hints.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 721070bb..231e4ecd 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -157,6 +157,13 @@ LinkHints = element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) isClickable = true + # Check for jsaction event listeners on the element. + if element.hasAttribute "jsaction" + jsactionRules = element.getAttribute("jsaction").split(";") + for jsactionRule in jsactionRules + ruleSplit = jsactionRule.split ":" + isClickable = true if ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none") + # Check for tagNames which are natively clickable. switch tagName when "a" -- cgit v1.2.3 From 3132ae601b2de787f9cddd3fd77b36767e2e467e Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 11:00:15 +0000 Subject: Complete a partially written comment --- content_scripts/link_hints.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 231e4ecd..57b46e6b 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -192,7 +192,7 @@ LinkHints = # + the ancestors of (1) and (2) have equal z-index, and # + the ancestor of (1) appears later in the DOM than the ancestor of (2). # - # Remove rects from + # Remove rects from elements where another clickable element lies above it. nonOverlappingElements = [] visibleElements = visibleElements.reverse() while visibleElement = visibleElements.pop() -- cgit v1.2.3 From 9c9c48598534c2a0cd8aec28a4a806d74f28e090 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 11:56:53 +0000 Subject: Move rect functions to their own file --- content_scripts/link_hints.coffee | 2 +- lib/dom_utils.coffee | 3 +- lib/rect.coffee | 64 +++++++++++++++++++++++++++++++++++++++ lib/utils.coffee | 60 ------------------------------------ manifest.json | 1 + tests/dom_tests/dom_tests.html | 1 + 6 files changed, 68 insertions(+), 63 deletions(-) create mode 100644 lib/rect.coffee diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 57b46e6b..27402250 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -198,7 +198,7 @@ LinkHints = while visibleElement = visibleElements.pop() rects = [visibleElement.rect] for {rect: negativeRect} in visibleElements - rects = Array::concat.apply [], (rects.map (rect) -> Utils.subtractRect rect, negativeRect) + rects = Array::concat.apply [], (rects.map (rect) -> Rect.subtract rect, negativeRect) if rects.length > 0 nonOverlappingElements.push {element: visibleElement.element, rect: rects[0]} else diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 7e19a7fc..ebbed006 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -95,7 +95,6 @@ DomUtils = else rect - getClientRectsForAreas: (imgClientRect, areas) -> rects = [] for area in areas @@ -114,7 +113,7 @@ DomUtils = # something more sophisticated, but likely not worth the effort. [x1, y1, x2, y2] = coords - rect = Utils.shiftRect (Utils.createRect x1, y1, x2, y2), imgClientRect.left, imgClientRect.top + rect = Rect.translate (Rect.create x1, y1, x2, y2), imgClientRect.left, imgClientRect.top rect = @cropRectToVisible rect rects.push {element: area, rect: rect} unless not rect or isNaN rect.top diff --git a/lib/rect.coffee b/lib/rect.coffee new file mode 100644 index 00000000..67c9de7c --- /dev/null +++ b/lib/rect.coffee @@ -0,0 +1,64 @@ +# Commands for manipulating rects. +Rect = + # Create a rect given the top left and bottom right corners. + create: (x1, y1, x2, y2) -> + bottom: y2 + top: y1 + left: x1 + right: x2 + width: x2 - x1 + height: y2 - y1 + + # Translate a rect by x horizontally and y vertically. + translate: (rect, x, y) -> + bottom: rect.bottom + y + top: rect.top + y + left: rect.left + x + right: rect.right + x + width: rect.width + height: rect.height + + # Subtract rect2 from rect1, returning an array of rects which are in rect1 but not rect2. + subtract: (rect1, rect2_) -> + # Bound rect2 by rect1 + rect2 = {} + rect2 = @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) + ) + + # If bounding rect2 has made the width or height negative, rect1 does not contain rect2. + return [rect1] if rect2.width < 0 or rect2.height < 0 + + # + # All the possible rects, in the order + # +-+-+-+ + # |1|2|3| + # +-+-+-+ + # |4| |5| + # +-+-+-+ + # |6|7|8| + # +-+-+-+ + # where the outer rectangle is rect1 and the inner rectangle is rect 2. Note that the rects may be of + # width or height 0. + # + rects = [ + # Top row. + @create rect1.left, rect1.top, rect2.left, rect2.top + @create rect2.left, rect1.top, rect2.right, rect2.top + @create rect2.right, rect1.top, rect1.right, rect2.top + # Middle row. + @create rect1.left, rect2.top, rect2.left, rect2.bottom + @create rect2.right, rect2.top, rect1.right, rect2.bottom + # Bottom row. + @create rect1.left, rect2.bottom, rect2.left, rect1.bottom + @create rect2.left, rect2.bottom, rect2.right, rect1.bottom + @create rect2.right, rect2.bottom, rect1.right, rect1.bottom + ] + + rects.filter (rect) -> rect.height > 0 and rect.width > 0 + +root = exports ? window +root.Rect = Rect diff --git a/lib/utils.coffee b/lib/utils.coffee index 6cc45f32..b7f8731a 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -136,66 +136,6 @@ Utils = # locale-sensitive uppercase detection hasUpperCase: (s) -> s.toLowerCase() != s - # Create a rect given the top left and bottom right corners. - createRect: (x1, y1, x2, y2) -> - bottom: y2 - top: y1 - left: x1 - right: x2 - width: x2 - x1 - height: y2 - y1 - - # Translate a rect by x horizontally and y vertically. - shiftRect: (rect, x, y) -> - bottom: rect.bottom + y - top: rect.top + y - left: rect.left + x - right: rect.right + x - width: rect.width - height: rect.height - - # Subtract rect2 from rect1, returning an array of rects which are in rect1 but not rect2. - subtractRect: (rect1, rect2_) -> - # Bound rect2 by rect1 - rect2 = {} - rect2 = @createRect( - Math.max(rect1.left, rect2_.left), - Math.max(rect1.top, rect2_.top), - Math.min(rect1.right, rect2_.right), - Math.min(rect1.bottom, rect2_.bottom) - ) - - # If bounding rect2 has made the width or height negative, rect1 does not contain rect2. - return [rect1] if rect2.width < 0 or rect2.height < 0 - - # - # All the possible rects, in the order - # +-+-+-+ - # |1|2|3| - # +-+-+-+ - # |4| |5| - # +-+-+-+ - # |6|7|8| - # +-+-+-+ - # where the outer rectangle is rect1 and the inner rectangle is rect 2. Note that the rects may be of - # width or height 0. - # - rects = [ - # Top row. - @createRect rect1.left, rect1.top, rect2.left, rect2.top - @createRect rect2.left, rect1.top, rect2.right, rect2.top - @createRect rect2.right, rect1.top, rect1.right, rect2.top - # Middle row. - @createRect rect1.left, rect2.top, rect2.left, rect2.bottom - @createRect rect2.right, rect2.top, rect1.right, rect2.bottom - # Bottom row. - @createRect rect1.left, rect2.bottom, rect2.left, rect1.bottom - @createRect rect2.left, rect2.bottom, rect2.right, rect1.bottom - @createRect rect2.right, rect2.bottom, rect1.right, rect1.bottom - ] - - rects.filter (rect) -> rect.height > 0 and rect.width > 0 - # 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. Function::curry = -> diff --git a/manifest.json b/manifest.json index 3cd88d1e..e5271692 100644 --- a/manifest.json +++ b/manifest.json @@ -35,6 +35,7 @@ "js": ["lib/utils.js", "lib/keyboard_utils.js", "lib/dom_utils.js", + "lib/rect.js", "lib/handler_stack.js", "lib/clipboard.js", "content_scripts/link_hints.js", diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index feddafac..d8232892 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -32,6 +32,7 @@ + -- cgit v1.2.3 From 91bb7d7b85df3b90882e92aeae2fa2021f61733e Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 12:59:35 +0000 Subject: Add tests for lib/rect --- lib/rect.coffee | 34 ++++-- tests/unit_tests/rect_test.coffee | 232 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 tests/unit_tests/rect_test.coffee diff --git a/lib/rect.coffee b/lib/rect.coffee index 67c9de7c..adc1fc36 100644 --- a/lib/rect.coffee +++ b/lib/rect.coffee @@ -9,8 +9,16 @@ Rect = width: x2 - x1 height: y2 - y1 + copy: (rect) -> + bottom: rect.bottom + top: rect.top + left: rect.left + right: rect.right + width: rect.width + height: rect.height + # Translate a rect by x horizontally and y vertically. - translate: (rect, x, y) -> + translate: (rect, x = 0, y = 0) -> bottom: rect.bottom + y top: rect.top + y left: rect.left + x @@ -19,18 +27,17 @@ Rect = height: rect.height # Subtract rect2 from rect1, returning an array of rects which are in rect1 but not rect2. - subtract: (rect1, rect2_) -> + subtract: (rect1, rect2) -> # Bound rect2 by rect1 - rect2 = {} rect2 = @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) + Math.max(rect1.left, rect2.left), + Math.max(rect1.top, rect2.top), + Math.min(rect1.right, rect2.right), + Math.min(rect1.bottom, rect2.bottom) ) # If bounding rect2 has made the width or height negative, rect1 does not contain rect2. - return [rect1] if rect2.width < 0 or rect2.height < 0 + return [Rect.copy rect1] if rect2.width < 0 or rect2.height < 0 # # All the possible rects, in the order @@ -60,5 +67,16 @@ Rect = rects.filter (rect) -> rect.height > 0 and rect.width > 0 + contains: (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] + true + root = exports ? window root.Rect = Rect diff --git a/tests/unit_tests/rect_test.coffee b/tests/unit_tests/rect_test.coffee new file mode 100644 index 00000000..cfb26b05 --- /dev/null +++ b/tests/unit_tests/rect_test.coffee @@ -0,0 +1,232 @@ +require "./test_helper.js" +extend(global, require "../../lib/rect.js") + +context "Rect", + should "set rect properties correctly", -> + [x1, y1, x2, y2] = [1, 2, 3, 4] + rect = Rect.create x1, y1, x2, y2 + assert.equal rect.left, x1 + assert.equal rect.top, y1 + assert.equal rect.right, x2 + assert.equal rect.bottom, y2 + assert.equal rect.width, x2 - x1 + assert.equal rect.height, y2 - y1 + + should "translate rect horizontally", -> + [x1, y1, x2, y2] = [1, 2, 3, 4] + x = 5 + rect1 = Rect.create x1, y1, x2, y2 + rect2 = Rect.translate rect1, x + + assert.equal rect1.left + x, rect2.left + assert.equal rect1.right + x, rect2.right + + assert.equal rect1.width, rect2.width + assert.equal rect1.height, rect2.height + assert.equal rect1.top, rect2.top + assert.equal rect1.bottom, rect2.bottom + + should "translate rect vertically", -> + [x1, y1, x2, y2] = [1, 2, 3, 4] + y = 5 + rect1 = Rect.create x1, y1, x2, y2 + rect2 = Rect.translate rect1, undefined, y + + assert.equal rect1.top + y, rect2.top + assert.equal rect1.bottom + y, rect2.bottom + + assert.equal rect1.width, rect2.width + assert.equal rect1.height, rect2.height + assert.equal rect1.left, rect2.left + assert.equal rect1.right, rect2.right + +context "Rect subtraction", + context "unchanged by rects outside", + should "left, above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -2, -2, -1, -1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "left", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -2, 0, -1, 1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "left, below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -2, 2, -1, 3 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right, above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 2, -2, 3, -1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 2, 0, 3, 1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right, below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 2, 2, 3, 3 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 0, -2, 1, -1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 0, 2, 1, 3 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + context "unchanged by rects touching", + should "left, above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -1, -1, 0, 0 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "left", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -1, 0, 0, 1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "left, below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create -1, 1, 0, 2 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right, above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 1, -1, 2, 0 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 1, 0, 2, 1 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "right, below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 1, 1, 2, 2 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "above", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 0, -1, 1, 0 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "below", -> + rect1 = Rect.create 0, 0, 1, 1 + rect2 = Rect.create 0, 1, 1, 2 + + rects = Rect.subtract rect1, rect2 + assert.equal rects.length, 1 + rect = rects[0] + assert.isTrue Rect.equals rect1, rect + + should "have nothing when subtracting itself", -> + rect = Rect.create 0, 0, 1, 1 + rects = Rect.subtract rect, rect + assert.equal rects.length, 0 + + should "not overlap subtracted rect", -> + rect = Rect.create 0, 0, 3, 3 + for x in [-2..2] + for y in [-2..2] + for width in [1..3] + for height in [1..3] + subtractRect = Rect.create x, y, (x + width), (y + height) + resultRects = Rect.subtract rect, subtractRect + for resultRect in resultRects + assert.isFalse Rect.contains subtractRect, resultRect + + should "be contained in original rect", -> + rect = Rect.create 0, 0, 3, 3 + for x in [-2..2] + for y in [-2..2] + for width in [1..3] + for height in [1..3] + subtractRect = Rect.create x, y, (x + width), (y + height) + resultRects = Rect.subtract rect, subtractRect + for resultRect in resultRects + assert.isTrue Rect.contains rect, resultRect + + should "contain the subtracted rect in the original minus the results", -> + rect = Rect.create 0, 0, 3, 3 + for x in [-2..2] + for y in [-2..2] + for width in [1..3] + for height in [1..3] + subtractRect = Rect.create x, y, (x + width), (y + height) + resultRects = Rect.subtract rect, subtractRect + resultComplement = [Rect.copy rect] + for resultRect in resultRects + resultComplement = Array::concat.apply [], + (resultComplement.map (rect) -> Rect.subtract rect, resultRect) + assert.isTrue (resultComplement.length == 0 or resultComplement.length == 1) + if resultComplement.length == 1 + complementRect = resultComplement[0] + assert.isTrue Rect.contains subtractRect, complementRect -- cgit v1.2.3 From ef863e5748c088f80ec9a0ffcaa06201c42e6c98 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 13:23:25 +0000 Subject: Make some minor changes/tweaks to rect handling in dom_utils --- lib/dom_utils.coffee | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index ebbed006..df1db3b9 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -68,8 +68,7 @@ DomUtils = else clientRect = @cropRectToVisible clientRect - if (!clientRect || clientRect.width < 3 || clientRect.height < 3) - continue + continue unless clientRect # eliminate invisible elements (see test_harnesses/visibility_test.html) computedStyle = window.getComputedStyle(element, null) @@ -81,33 +80,42 @@ DomUtils = null + # + # Bounds the rect by the current viewport dimensions. If the rect is offscreen or has a height or width < 3 + # then null is returned instead of a rect. + # cropRectToVisible: (rect) -> - if (rect.top < 0) - rect.height += rect.top - rect.top = 0 - - if (rect.left < 0) - rect.width += rect.left - rect.left = 0 - - if (rect.top >= window.innerHeight - 4 || rect.left >= window.innerWidth - 4) + boundedRect = Rect.create( + Math.max(rect.left, 0), + Math.max(rect.top, 0), + Math.min(rect.right, window.innerWidth), + Math.min(rect.bottom, window.innerHeight) + ) + if boundedRect.width < 3 or boundedRect.height < 3 null else - rect + boundedRect + # + # Get the client rects for the elements in a based on the position of the element using + # the map. Returns an array of rects. + # getClientRectsForAreas: (imgClientRect, areas) -> rects = [] for area in areas coords = area.coords.split(",").map((coord) -> parseInt(coord, 10)) shape = area.shape.toLowerCase() - if shape == "rect" or coords.length == 4 + if shape in ["rect", "rectangle"] # "rectangle" is an IE non-standard. [x1, y1, x2, y2] = coords - else if shape == "circle" or coords.length == 3 + else if shape in ["circle", "circ"] # "circ" is an IE non-standard. [x, y, r] = coords - x1 = x - r - x2 = x + r - y1 = y - r - y2 = y + r + diff = r / Math.sqrt 2 # Gives us an inner square + x1 = x - diff + x2 = x + diff + y1 = y - diff + y2 = y + diff + else if shape == "default" + [x1, y1, x2, y2] = [0, 0, imgClientRect.width, imgClientRect.height] else # Just consider the rectangle surrounding the first two points in a polygon. It's possible to do # something more sophisticated, but likely not worth the effort. @@ -116,7 +124,7 @@ DomUtils = rect = Rect.translate (Rect.create x1, y1, x2, y2), imgClientRect.left, imgClientRect.top rect = @cropRectToVisible rect - rects.push {element: area, rect: rect} unless not rect or isNaN rect.top + rects.push {element: area, rect: rect} if rect and not isNaN rect.top rects # -- cgit v1.2.3 From 845fd65e1c1a52329352f5068e3c7f0ef7b26154 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 13:25:21 +0000 Subject: Use Rect.copy instead of literal member by member copy of a rect --- lib/dom_utils.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index df1db3b9..7fd126b8 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -46,10 +46,7 @@ DomUtils = # getVisibleClientRect: (element) -> # Note: this call will be expensive if we modify the DOM in between calls. - clientRects = ({ - top: clientRect.top, right: clientRect.right, bottom: clientRect.bottom, left: clientRect.left, - width: clientRect.width, height: clientRect.height - } for clientRect in element.getClientRects()) + clientRects = (Rect.copy clientRect for clientRect in element.getClientRects()) for clientRect in clientRects # If the link has zero dimensions, it may be wrapping visible -- cgit v1.2.3 From 93a5a571cc06087b2abfe383424c2fcc6ef02358 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 13:29:46 +0000 Subject: Split textarea and input detection in link hints --- content_scripts/link_hints.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 27402250..95026cba 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -168,12 +168,15 @@ LinkHints = switch tagName when "a" isClickable = true - when "textarea", "input" - unless (tagName == "input" and element.getAttribute("type")?.toLowerCase() == "hidden") or - element.disabled or (element.readOnly and DomUtils.isSelectable element) + when "textarea" + isClickable = not element.disabled and not element.readOnly + when "input" + unless element.getAttribute("type")?.toLowerCase() == "hidden" or + element.disabled or + (element.readOnly and DomUtils.isSelectable element) isClickable = true when "button", "select" - isClickable = not element.disabled + isClickable = true unless element.disabled continue unless isClickable # If the element isn't clickable, do nothing. clientRect = DomUtils.getVisibleClientRect element -- cgit v1.2.3 From aa047d5b4b6124f7e2d1230ab590b9244db6ebb3 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 13:39:22 +0000 Subject: Improve comments for LinkHints.getVisibleClickableElements --- content_scripts/link_hints.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 95026cba..b1c44e42 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -125,9 +125,11 @@ LinkHints = marker # - # Returns all clickable elements that are not hidden and are in the current viewport. - # We prune invisible elements partly for performance reasons, but moreso it's to decrease the number - # of digits needed to enumerate all of the links on screen. + # Returns all clickable elements that are not hidden and are in the current viewport, along with rectangles + # at which (parts of) the elements are displayed. + # In the process, we try to find rects where elements do not overlap so that link hints are unambiguous. + # Because of this, the rects returned will frequently *NOT* be equivalent to the rects for the whole + # element. # getVisibleClickableElements: -> elements = document.documentElement.getElementsByTagName "*" @@ -197,6 +199,7 @@ LinkHints = # # Remove rects from elements where another clickable element lies above it. nonOverlappingElements = [] + # Traverse the DOM from first to last, since later elements show above earlier elements. visibleElements = visibleElements.reverse() while visibleElement = visibleElements.pop() rects = [visibleElement.rect] -- cgit v1.2.3 From e56dea52d4e0eead061f676891c04cfc07336194 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 15:08:59 +0000 Subject: Add brackets so the code compiles as expected --- content_scripts/link_hints.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index b1c44e42..ba1603e4 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -152,9 +152,9 @@ LinkHints = visibleElements = visibleElements.concat areaRects # Check for attributes that make an element clickable regardless of its tagName. - if (element.hasAttribute "onclick" or - element.hasAttribute "tabindex" or - element.getAttribute "role" in ["button", "link"] or + if (element.hasAttribute("onclick") or + element.hasAttribute("tabindex") or + element.getAttribute("role")?.toLowerCase() in ["button", "link"] or element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) isClickable = true -- cgit v1.2.3 From 4cfdc55b2054f3b00daf2aa2d8ffd482b4e3aaf9 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 18 Dec 2014 22:46:17 +0000 Subject: Don't show a link hint for certain link hint elements Disables showing link hint for elements which * are identified as clickableonly by the tabindex attribute, and * have the entirety of their contents overlapped by other clickable elements. This removes some redundant link hints that were visible on Google+, and hopefully shouldn't remove any useful link hints. --- content_scripts/link_hints.coffee | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index ba1603e4..6934e5b8 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -138,6 +138,7 @@ LinkHints = for element in elements tagName = element.tagName.toLowerCase() isClickable = false + onlyHasTabIndex = false # Insert area elements that provide click functionality to an img. if tagName == "img" @@ -153,7 +154,6 @@ LinkHints = # Check for attributes that make an element clickable regardless of its tagName. if (element.hasAttribute("onclick") or - element.hasAttribute("tabindex") or element.getAttribute("role")?.toLowerCase() in ["button", "link"] or element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) @@ -180,10 +180,15 @@ LinkHints = when "button", "select" isClickable = true unless element.disabled + # Elements with tabindex are sometimes useful, but usually not. We can treat them as second class + # citizens when it improves UX, so take special note of them. + if element.hasAttribute("tabindex") and not isClickable + isClickable = onlyHasTabIndex = true + continue unless isClickable # If the element isn't clickable, do nothing. clientRect = DomUtils.getVisibleClientRect element if clientRect != null - visibleElements.push {element: element, rect: clientRect} + visibleElements.push {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} # TODO(mrmr1993): Consider z-index. z-index affects behviour as follows: # * The document has a local stacking context. @@ -209,10 +214,10 @@ LinkHints = nonOverlappingElements.push {element: visibleElement.element, rect: rects[0]} else # Every part of the element is covered by some other element, so just insert the whole element's - # rect. + # rect. Except for elements with tabIndex set; these are often more trouble than they're worth. # TODO(mrmr1993): This is probably the wrong thing to do, but we don't want to stop being able to # click some elements that we could click before. - nonOverlappingElements.push visibleElement + nonOverlappingElements.push visibleElement unless visibleElement.onlyHasTabIndex nonOverlappingElements -- cgit v1.2.3 From 1059f98d5c9a552b2fa3fbcdddc7e44d0676056e Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Fri, 19 Dec 2014 01:10:41 +0000 Subject: Detect aria properties for disabling/hiding elements in link hints --- content_scripts/link_hints.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 6934e5b8..fa7fa937 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -152,6 +152,11 @@ LinkHints = areaRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas visibleElements = visibleElements.concat areaRects + # Check aria properties to see if the element should be ignored. + if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or + element.getAttribute("aria-disabled")?.toLowerCase() in ["", "true"]) + continue # No point continuing the loop; this element should never have a link hint + # Check for attributes that make an element clickable regardless of its tagName. if (element.hasAttribute("onclick") or element.getAttribute("role")?.toLowerCase() in ["button", "link"] or -- cgit v1.2.3 From 226cfb8011e83e48b9c723ebc891ad0e9a7c82ec Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 05:38:29 +0000 Subject: Tidy cursor hider, move haveChromeVersion to utils. --- content_scripts/vimium_frontend.coffee | 10 ++++------ lib/utils.coffee | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 6f099e54..21a05de6 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1107,9 +1107,7 @@ Tween = CursorHider = # - # Hides the cursor when the browser scrolls, and prevent mouse from hovering while invisible. - # Disabled for Chrome versions less than 39.0.2171.71 due to a browser error. - # NOTE(smblott) onScroll and onMouseMove events come in pairs. + # Hide the cursor when the browser scrolls, and prevent mouse from hovering while invisible. # cursorHideStyle: null isScrolling: false @@ -1125,9 +1123,9 @@ CursorHider = CursorHider.isScrolling = false init: -> - # Disable for Chrome versions less than 39.0.2171.71 due to a browser error. - chromeVersion = navigator.appVersion.match(/Chrome\/(.*?) /)?[1] || "" - return if 0 <= Utils.compareVersions "39.0.2171.71", chromeVersion + # Disable cursor hiding for Chrome versions less than 39.0.2171.71 due to a suspected browser error. + # See #1345 and #1348. + return unless Utils.haveChromeVersion "39.0.2171.71" @cursorHideStyle = document.createElement("style") @cursorHideStyle.innerHTML = """ diff --git a/lib/utils.coffee b/lib/utils.coffee index 57d8a488..2efb4716 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -124,6 +124,11 @@ Utils = return 1 0 + # True if the current Chrome version is at least the required version. + haveChromeVersion: (required) -> + chromeVersion = navigator.appVersion.match(/Chrome\/(.*?) /)?[1] + chromeVersion and 0 <= Utils.compareVersions chromeVersion, required + # Zip two (or more) arrays: # - Utils.zip([ [a,b], [1,2] ]) returns [ [a,1], [b,2] ] # - Length of result is `arrays[0].length`. -- cgit v1.2.3 From 7a99764137d600dc65e76da5093e91d2ecd5eaeb Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 06:37:14 +0000 Subject: Disable cursor hider. See #1359. There appear to be common cases where the cursor hider doesn't hide the cursor. So the UX is inconsistent. We need to consider whether this is fixable or acceptable. --- content_scripts/vimium_frontend.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 21a05de6..8d8a67e4 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1123,6 +1123,10 @@ CursorHider = CursorHider.isScrolling = false init: -> + # Temporarily disabled pending consideration of #1359 (in particular, whether cursor hiding is too fragile + # as to provide a consistent UX). + return + # Disable cursor hiding for Chrome versions less than 39.0.2171.71 due to a suspected browser error. # See #1345 and #1348. return unless Utils.haveChromeVersion "39.0.2171.71" -- cgit v1.2.3 From 0506ea78ebcd003b47db4f5587e07251f7c8682b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 07:10:02 +0000 Subject: Initialize scroller to first scrollable element. See #1358. --- content_scripts/scroller.coffee | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 2f69fc7d..48b99cff 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -84,6 +84,16 @@ findScrollableElement = (element, direction, amount, factor) -> element = element.parentElement || document.body element +# On some pages, document.body is not scrollable. Here, we search the document for the first visible element +# which does scroll vertically. This is used to initialize activatedElement. See #1358. +firstScrollableElement = (element=document.body) -> + if doesScroll element, "y", 1, 1 + element + else + for child in element.children + return ele if DomUtils.getVisibleClientRect(child) and ele = firstScrollableElement child + null + checkVisibility = (element) -> # If the activated element has been scrolled completely offscreen, then subsequent changes in its scroll # position will not provide any more visual feedback to the user. Therefore, we deactivate it so that @@ -206,7 +216,7 @@ Scroller = window.scrollBy(0, amount) return - activatedElement ||= document.body + activatedElement ||= firstScrollableElement() || document.body return unless activatedElement # Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth, @@ -218,7 +228,7 @@ Scroller = scrollTo: (direction, pos) -> return unless document.body or activatedElement - activatedElement ||= document.body + activatedElement ||= firstScrollableElement() || document.body element = findScrollableElement activatedElement, direction, pos, 1 amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] -- cgit v1.2.3 From ac2e1077107d72ef82e1424634aa86945696e6b4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 08:35:03 +0000 Subject: Initialize scroller to *largest* visible scrollable element. --- content_scripts/scroller.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 48b99cff..dec817a1 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -84,14 +84,17 @@ findScrollableElement = (element, direction, amount, factor) -> element = element.parentElement || document.body element -# On some pages, document.body is not scrollable. Here, we search the document for the first visible element -# which does scroll vertically. This is used to initialize activatedElement. See #1358. +# On some pages, document.body is not scrollable. Here, we search the document for the largest visible +# element which does scroll vertically. This is used to initialize activatedElement. See #1358. firstScrollableElement = (element=document.body) -> if doesScroll element, "y", 1, 1 element else - for child in element.children - return ele if DomUtils.getVisibleClientRect(child) and ele = firstScrollableElement child + children = ({element: child, rect: DomUtils.getVisibleClientRect(child)} for child in element.children) + children = children.filter (child) -> child.rect # Filter out non-visible elements. + children.map (child) -> child.area = child.rect.width * child.rect.height + for child in children.sort((a,b) -> b.area - a.area) # Largest to smallest by visible area. + return ele if ele = firstScrollableElement child.element null checkVisibility = (element) -> -- cgit v1.2.3 From 24b968943acddd224dd795c1e26425d3b75520e2 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 08:49:01 +0000 Subject: Delay initialization of activeElement. We could incorrectly initialize activeElement to document.body if the scroller is called too early; so delay initialization. It's safe to leave activeElement as null. --- content_scripts/scroller.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index dec817a1..3692a002 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -219,7 +219,7 @@ Scroller = window.scrollBy(0, amount) return - activatedElement ||= firstScrollableElement() || document.body + activatedElement ||= firstScrollableElement() return unless activatedElement # Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth, @@ -230,8 +230,8 @@ Scroller = CoreScroller.scroll element, direction, elementAmount scrollTo: (direction, pos) -> - return unless document.body or activatedElement - activatedElement ||= firstScrollableElement() || document.body + activatedElement ||= firstScrollableElement() + return unless activatedElement element = findScrollableElement activatedElement, direction, pos, 1 amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] -- cgit v1.2.3 From 5f4ae0f11340c0c7385d8a91228941128a9731da Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 19 Dec 2014 11:25:51 +0000 Subject: Also test negative direction when initializing scroller. The right scrollable element to choose may be scrolled to the bottom, so we won't find it if we only test scrolling down. We need to test scrolling up as well. --- content_scripts/scroller.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 3692a002..fdfb7ddc 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -67,7 +67,7 @@ shouldScroll = (element, direction) -> # Instead, we scroll the element by 1 or -1 and see if it moved (then put it back). :factor is the factor by # which :scrollBy and :scrollTo will later scale the scroll amount. :factor can be negative, so we need it # here in order to decide whether we should test a forward scroll or a backward scroll. -# Bug verified in Chrome 38.0.2125.104. +# Bug last verified in Chrome 38.0.2125.104. doesScroll = (element, direction, amount, factor) -> # amount is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls (only # gg, G, and friends), amount can be either a string ("max" or "viewSize") or zero. In the former case, @@ -87,7 +87,7 @@ findScrollableElement = (element, direction, amount, factor) -> # On some pages, document.body is not scrollable. Here, we search the document for the largest visible # element which does scroll vertically. This is used to initialize activatedElement. See #1358. firstScrollableElement = (element=document.body) -> - if doesScroll element, "y", 1, 1 + if doesScroll(element, "y", 1, 1) or doesScroll(element, "y", -1, 1) element else children = ({element: child, rect: DomUtils.getVisibleClientRect(child)} for child in element.children) -- cgit v1.2.3 From 3a688f754ebd647ce56b33d18c5744759c5efe95 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sat, 20 Dec 2014 16:18:28 +0000 Subject: Use ||= to not ignore some clickable elements, no negative tabindex Elements with `tabindex="n"` for parseInt(n) < 0 cannot be selected by pressing the tab key, according to the spec. If we have no other reason to suspect that the element is clickable, we may as well ignore them. --- content_scripts/link_hints.coffee | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index fa7fa937..9eb7b87c 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -176,18 +176,19 @@ LinkHints = when "a" isClickable = true when "textarea" - isClickable = not element.disabled and not element.readOnly + isClickable ||= not element.disabled and not element.readOnly when "input" - unless element.getAttribute("type")?.toLowerCase() == "hidden" or - element.disabled or - (element.readOnly and DomUtils.isSelectable element) - isClickable = true + isClickable ||= not (element.getAttribute("type")?.toLowerCase() == "hidden" or + element.disabled or + (element.readOnly and DomUtils.isSelectable element)) when "button", "select" - isClickable = true unless element.disabled + isClickable ||= not element.disabled # Elements with tabindex are sometimes useful, but usually not. We can treat them as second class # citizens when it improves UX, so take special note of them. - if element.hasAttribute("tabindex") and not isClickable + tabIndexValue = element.getAttribute("tabindex") + tabIndex = if tabIndexValue == "" then 0 else parseInt tabIndexValue + unless isClickable or isNaN(tabIndex) or tabIndex < 0 isClickable = onlyHasTabIndex = true continue unless isClickable # If the element isn't clickable, do nothing. -- cgit v1.2.3 From 56ba56622c529d570285f42732f4f1ed29830987 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 11:51:06 +0000 Subject: Support small s with link hints This is primarily to deal with our calculated rects being too small for the `` on http://www.mapsofindia.com/worldmap/clickable-world-map.html --- lib/dom_utils.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 7fd126b8..8bb099a1 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -59,13 +59,13 @@ DomUtils = continue if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute') childClientRect = @getVisibleClientRect(child) - continue if (childClientRect == null) + continue if clientRect == null return childClientRect else clientRect = @cropRectToVisible clientRect - continue unless clientRect + continue if clientRect == null or clientRect.width < 3 or clientRect.height < 3 # eliminate invisible elements (see test_harnesses/visibility_test.html) computedStyle = window.getComputedStyle(element, null) @@ -83,12 +83,12 @@ DomUtils = # cropRectToVisible: (rect) -> boundedRect = Rect.create( - Math.max(rect.left, 0), - Math.max(rect.top, 0), - Math.min(rect.right, window.innerWidth), - Math.min(rect.bottom, window.innerHeight) + Math.max(rect.left, 0) + Math.max(rect.top, 0) + rect.right + rect.bottom ) - if boundedRect.width < 3 or boundedRect.height < 3 + if boundedRect.top >= window.innerHeight - 4 or boundedRect.left >= window.innerWidth - 4 null else boundedRect -- cgit v1.2.3 From 15d094675c4e354becf58b5f66754dd939d5daeb Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 11:54:49 +0000 Subject: Rename a poorly named variable --- content_scripts/link_hints.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 9eb7b87c..616d40ee 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -149,8 +149,8 @@ LinkHints = map = document.querySelector "map[name=\"#{mapName}\"]" if map and imgClientRects.length > 0 areas = map.getElementsByTagName "area" - areaRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas - visibleElements = visibleElements.concat areaRects + areasAndRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas + visibleElements = visibleElements.concat areasAndRects # Check aria properties to see if the element should be ignored. if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or -- cgit v1.2.3 From 76af56da84753163adc4dbf943374a10f0cb8321 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 11:56:18 +0000 Subject: Use push with a splat rather than concat --- content_scripts/link_hints.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 616d40ee..c4bf3f96 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -150,7 +150,7 @@ LinkHints = if map and imgClientRects.length > 0 areas = map.getElementsByTagName "area" areasAndRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas - visibleElements = visibleElements.concat areasAndRects + visibleElements.push areasAndRects... # Check aria properties to see if the element should be ignored. if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or -- cgit v1.2.3 From e79537231f35215339c04d1e01347974bd4bd810 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 12:33:14 +0000 Subject: Correct a typo, add some tests for consistency --- lib/dom_utils.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 8bb099a1..2447b4bb 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -59,7 +59,7 @@ DomUtils = continue if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute') childClientRect = @getVisibleClientRect(child) - continue if clientRect == null + continue if childClientRect == null or childClientRect.width < 3 or childClientRect.height < 3 return childClientRect else -- cgit v1.2.3 From c1ffbc88ed1e340a7a046e1d75499642bf220e7f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 12:35:13 +0000 Subject: Prefer `||=` to `= true if` --- content_scripts/link_hints.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index c4bf3f96..df442ea6 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -169,7 +169,7 @@ LinkHints = jsactionRules = element.getAttribute("jsaction").split(";") for jsactionRule in jsactionRules ruleSplit = jsactionRule.split ":" - isClickable = true if ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none") + isClickable ||= ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none") # Check for tagNames which are natively clickable. switch tagName -- cgit v1.2.3 From 278820f544d201b975a73ebe82d4a67792b967df Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 22 Dec 2014 12:39:47 +0000 Subject: Use a splat instead of apply --- content_scripts/link_hints.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index df442ea6..3a9e2027 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -215,7 +215,8 @@ LinkHints = while visibleElement = visibleElements.pop() rects = [visibleElement.rect] for {rect: negativeRect} in visibleElements - rects = Array::concat.apply [], (rects.map (rect) -> Rect.subtract rect, negativeRect) + # Subtract negativeRect from every rect in rects, and concatenate the arrays of rects that result. + rects = [].concat (rects.map (rect) -> Rect.subtract rect, negativeRect)... if rects.length > 0 nonOverlappingElements.push {element: visibleElement.element, rect: rects[0]} else -- cgit v1.2.3 From d7597f407ea7932b1f985160c3f688801d035fcc Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Wed, 29 Oct 2014 21:34:32 +0000 Subject: Reintroduce 2c7bebb5f2c873850c2b2d82013cab4eb3d4913c --- content_scripts/vimium_frontend.coffee | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 2de612d4..90ac227a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -436,15 +436,13 @@ onKeydown = (event) -> keyChar = "<" + keyChar + ">" if (isInsertMode() && KeyboardUtils.isEscape(event)) - # Note that we can't programmatically blur out of Flash embeds from Javascript. - if (!isEmbed(event.srcElement)) + if isEditable(event.srcElement) or isEmbed(event.srcElement) # Remove focus so the user can't just get himself back into insert mode by typing in the same input # box. - if (isEditable(event.srcElement)) - event.srcElement.blur() - exitInsertMode() - DomUtils.suppressEvent event - KeydownEvents.push event + event.srcElement.blur() + exitInsertMode() + DomUtils.suppressEvent event + handledKeydownEvents.push event else if (findMode) if (KeyboardUtils.isEscape(event)) -- cgit v1.2.3 From 99d784ff593378be41b5e6eb87867826fee9e921 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 22 Dec 2014 16:56:29 +0000 Subject: Add comment regarding .blur() on embeds. Including embeds for .blur() etc. here is experimental. It appears to be the right thing to do for most common use cases. However, it could also cripple flash-based sites and games. See discussion in #1211 and #1194. --- content_scripts/vimium_frontend.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index eacba682..071e87a6 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -458,6 +458,9 @@ onKeydown = (event) -> if isEditable(event.srcElement) or isEmbed(event.srcElement) # Remove focus so the user can't just get himself back into insert mode by typing in the same input # box. + # NOTE(smblott, 2014/12/22) Including embeds for .blur() etc. here is experimental. It appears to be + # the right thing to do for most common use cases. However, it could also cripple flash-based sites and + # games. See discussion in #1211 and #1194. event.srcElement.blur() exitInsertMode() DomUtils.suppressEvent event -- cgit v1.2.3 From b26f4aed8585418d18dfc43262070c8c8741e5e3 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 28 Dec 2014 19:24:33 +0000 Subject: Add UIComponent code for iframes --- content_scripts/ui_component.coffee | 66 +++++++++++++++++++++++++++++++++++++ manifest.json | 1 + 2 files changed, 67 insertions(+) create mode 100644 content_scripts/ui_component.coffee diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee new file mode 100644 index 00000000..c0889e7f --- /dev/null +++ b/content_scripts/ui_component.coffee @@ -0,0 +1,66 @@ +class UIComponent + iframeElement: null + iframePort: null + messageEventListeners: [] + showStyle: "" + hideStyle: "" + + constructor: (iframeUrl, className) -> + @iframeElement = document.createElement "iframe" + @iframeElement.className = className + @iframeElement.seamless = "seamless" + @iframeElement.src = chrome.runtime.getURL iframeUrl + @iframeElement.addEventListener "load", => @openPort() + document.documentElement.appendChild @iframeElement + @hide() + + # Open a port and pass it to the iframe via window.postMessage. + openPort: -> + messageChannel = new MessageChannel() + @iframePort = messageChannel.port1 + @iframePort.onmessage = (event) => @handleMessage event + + # Get iframeMessageSecret so the iframe can determine that our message isn't the page impersonating us. + chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) => + @iframeElement.contentWindow.postMessage secret, chrome.runtime.getURL(""), [messageChannel.port2] + + postMessage: (data) -> @iframePort.postMessage data + + # Execute each event listener on the current event until we get a falsy return value. + handleMessage: (event) -> + for listener in @messageEventListeners + retVal = listener.call this, event + return false unless retVal + true + + addEventListener: (type, listener) -> + if type == "message" + @messageEventListeners.push listener + undefined + + removeEventListener: (type, listener) -> + if type == "message" + listenerIndex = @messageEventListeners.indexOf listener + if listenerIndex > -1 + @messageEventListeners = @messageEventListeners.splice listenerIndex, 1 + undefined + + setHideStyle: (@hideStyle) -> + @hide() if @showing == false + + setShowStyle: (@showStyle) -> + @show() if @showing == true + + show: -> + return unless @iframeElement? + @iframeElement.setAttribute "style", @showStyle + @iframeElement.focus() + @showing = true + + hide: -> + return unless @iframeElement? + @iframeElement.setAttribute "style", @hideStyle + @showing = false + +root = exports ? window +root.UIComponent = UIComponent diff --git a/manifest.json b/manifest.json index 3cd88d1e..a92ed0da 100644 --- a/manifest.json +++ b/manifest.json @@ -37,6 +37,7 @@ "lib/dom_utils.js", "lib/handler_stack.js", "lib/clipboard.js", + "content_scripts/ui_component.js", "content_scripts/link_hints.js", "content_scripts/vomnibar.js", "content_scripts/scroller.js", -- cgit v1.2.3 From 020331fd8e120eee1fcb80ad05f7fcde33f3e981 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 28 Dec 2014 19:41:30 +0000 Subject: Create a script for connecting UIComponent iframes with their parents --- pages/ui_component_server.coffee | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pages/ui_component_server.coffee diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee new file mode 100644 index 00000000..240233ed --- /dev/null +++ b/pages/ui_component_server.coffee @@ -0,0 +1,41 @@ +# Register the port recieved from the parent window, and stop listening for messages on the window object. +window.addEventListener "message", (event) -> + return unless event.source == window.parent + currentFunction = arguments.callee + + # Check event.data against iframeMessageSecret so we can determine that this message hasn't been spoofed. + chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) -> + return unless event.data == secret + UIComponentServer.portOpen event.ports[0] + window.removeEventListener "message", currentFunction # Stop listening for message events. + +UIComponentServer = + ownerPagePort: null + messageEventListeners: [] + + portOpen: (@ownerPagePort) -> + @ownerPagePort.onmessage = (event) => @handleMessage event + + postMessage: (data) -> @ownerPagePort.postMessage data + + # Execute each event listener on the current event until we get a falsy return value. + handleMessage: (event) -> + for listener in @messageEventListeners + retVal = listener.call this, event + return false unless retVal + true + + addEventListener: (type, listener) -> + if type == "message" + @messageEventListeners.push listener + undefined + + removeEventListener: (type, listener) -> + if type == "message" + listenerIndex = @messageEventListeners.indexOf listener + if listenerIndex > -1 + @messageEventListeners = @messageEventListeners.splice listenerIndex, 1 + undefined + +root = exports ? window +root.UIComponentServer = UIComponentServer -- cgit v1.2.3 From 697b065f9adff48ad270d8ea85e90faa63190179 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 28 Dec 2014 19:51:45 +0000 Subject: Fix tests for UIComponent --- tests/dom_tests/chrome.coffee | 4 ++++ tests/dom_tests/dom_tests.html | 1 + 2 files changed, 5 insertions(+) diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee index 7f99e27f..2695ef20 100644 --- a/tests/dom_tests/chrome.coffee +++ b/tests/dom_tests/chrome.coffee @@ -17,5 +17,9 @@ root.chrome = { } sendMessage: -> getManifest: -> + getURL: (url) -> "../../#{url}" } + storage: + local: + get: -> } diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index feddafac..750af41d 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -34,6 +34,7 @@ + -- cgit v1.2.3 From d20caa49075c0605ec2196416a5633d5e205b615 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Sun, 28 Dec 2014 19:53:06 +0000 Subject: Add an example test for UIComponent --- content_scripts/vimium_frontend.coffee | 15 +++++++++++++++ manifest.json | 5 ++++- pages/test_ui_component.coffee | 2 ++ pages/test_ui_component.html | 10 ++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 pages/test_ui_component.coffee create mode 100644 pages/test_ui_component.html diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 1f116f88..245e481a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -181,6 +181,8 @@ window.addEventListener "focus", -> initializeOnDomReady = -> enterInsertModeIfElementIsFocused() if isEnabledForUrl + testUIComponentSetup() + # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) @@ -920,6 +922,10 @@ exitFindMode = -> window.showHelpDialog = (html, fid) -> return if (isShowingHelpDialog || !document.body || fid != frameId) + + testUIComponent.show() + testUIComponent.postMessage "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" + isShowingHelpDialog = true container = document.createElement("div") container.id = "vimiumHelpDialogContainer" @@ -966,6 +972,9 @@ window.showHelpDialog = (html, fid) -> hideHelpDialog = (clickEvent) -> + + testUIComponent.hide() + isShowingHelpDialog = false helpDialog = document.getElementById("vimiumHelpDialogContainer") if (helpDialog) @@ -1086,6 +1095,12 @@ Tween = value = (elapsed / state.duration) * (state.to - state.from) + state.from state.onUpdate(value) +testUIComponent = null +testUIComponentSetup = -> + testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent" + testUIComponent.setHideStyle "display: none;" + testUIComponent.setShowStyle "display: block;" + initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) window.addEventListener("unload", unregisterFrame) diff --git a/manifest.json b/manifest.json index a92ed0da..03676ebf 100644 --- a/manifest.json +++ b/manifest.json @@ -58,5 +58,8 @@ "browser_action": { "default_icon": "icons/browser_action_disabled.png", "default_popup": "pages/popup.html" - } + }, + "web_accessible_resources": [ + "pages/test_ui_component.html" + ] } diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee new file mode 100644 index 00000000..b1d3625a --- /dev/null +++ b/pages/test_ui_component.coffee @@ -0,0 +1,2 @@ +UIComponentServer.addEventListener "message", (event) -> + document.body.innerHTML = event.data diff --git a/pages/test_ui_component.html b/pages/test_ui_component.html new file mode 100644 index 00000000..b8ad7475 --- /dev/null +++ b/pages/test_ui_component.html @@ -0,0 +1,10 @@ + + + Test + + + + + + + -- cgit v1.2.3 From 6c1bbf0ab5781951364464c5fa68ad22f74c9fee Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 07:15:44 +0000 Subject: Scroller; check document.body exists. --- content_scripts/scroller.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index fdfb7ddc..889dc042 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -219,7 +219,7 @@ Scroller = window.scrollBy(0, amount) return - activatedElement ||= firstScrollableElement() + activatedElement ||= document.body and firstScrollableElement() return unless activatedElement # Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth, @@ -230,7 +230,7 @@ Scroller = CoreScroller.scroll element, direction, elementAmount scrollTo: (direction, pos) -> - activatedElement ||= firstScrollableElement() + activatedElement ||= document.body and firstScrollableElement() return unless activatedElement element = findScrollableElement activatedElement, direction, pos, 1 -- cgit v1.2.3 From 71af7f016f51e3c8b9c1fcfba46cb8289c91e030 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 08:12:59 +0000 Subject: IFrame framework; develop demo. --- background_scripts/commands.coffee | 3 +++ content_scripts/vimium_frontend.coffee | 10 ++++------ pages/test_ui_component.coffee | 5 +++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index 585ef572..63b870cc 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -91,6 +91,7 @@ Commands = commandGroups: pageNavigation: ["scrollDown", + "activateTestUIComponent", "scrollUp", "scrollLeft", "scrollRight", @@ -252,6 +253,7 @@ defaultKeyMappings = "m": "Marks.activateCreateMode" "`": "Marks.activateGotoMode" + "D": "activateTestUIComponent" # This is a mapping of: commandIdentifier => [description, options]. @@ -263,6 +265,7 @@ commandDescriptions = scrollUp: ["Scroll up"] scrollLeft: ["Scroll left"] scrollRight: ["Scroll right"] + activateTestUIComponent: ["UI component test"] scrollToTop: ["Scroll to the top of the page", { noRepeat: true }] scrollToBottom: ["Scroll to the bottom of the page", { noRepeat: true }] diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 245e481a..5c166ff0 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -923,9 +923,6 @@ exitFindMode = -> window.showHelpDialog = (html, fid) -> return if (isShowingHelpDialog || !document.body || fid != frameId) - testUIComponent.show() - testUIComponent.postMessage "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" - isShowingHelpDialog = true container = document.createElement("div") container.id = "vimiumHelpDialogContainer" @@ -972,9 +969,6 @@ window.showHelpDialog = (html, fid) -> hideHelpDialog = (clickEvent) -> - - testUIComponent.hide() - isShowingHelpDialog = false helpDialog = document.getElementById("vimiumHelpDialogContainer") if (helpDialog) @@ -1101,6 +1095,10 @@ testUIComponentSetup = -> testUIComponent.setHideStyle "display: none;" testUIComponent.setShowStyle "display: block;" +window.activateTestUIComponent = -> + testUIComponent.show() + testUIComponent.postMessage "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" + initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) window.addEventListener("unload", unregisterFrame) diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee index b1d3625a..0156c8f2 100644 --- a/pages/test_ui_component.coffee +++ b/pages/test_ui_component.coffee @@ -1,2 +1,7 @@ UIComponentServer.addEventListener "message", (event) -> document.body.innerHTML = event.data + +document.addEventListener "DOMContentLoaded", -> + document.addEventListener "keydown", (event) -> + # Close on any key. + console.log "How do I close myself?" -- cgit v1.2.3 From 5ea0f75a00b592956981bf8f6f7a0d2fa89620ae Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 09:33:34 +0000 Subject: Close UIComponent iframes when pressing esc by default --- content_scripts/ui_component.coffee | 11 +++++++++++ pages/test_ui_component.coffee | 5 ----- pages/test_ui_component.html | 1 + pages/ui_component_server.coffee | 9 +++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index c0889e7f..f47719e5 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -14,6 +14,8 @@ class UIComponent document.documentElement.appendChild @iframeElement @hide() + @addEventListener "message", handleHideMessage + # Open a port and pass it to the iframe via window.postMessage. openPort: -> messageChannel = new MessageChannel() @@ -30,6 +32,7 @@ class UIComponent handleMessage: (event) -> for listener in @messageEventListeners retVal = listener.call this, event + retVal ?= true return false unless retVal true @@ -60,7 +63,15 @@ class UIComponent hide: -> return unless @iframeElement? @iframeElement.setAttribute "style", @hideStyle + window.focus() @showing = false +handleHideMessage = (event) -> + if event.data == "hide" + @hide() + false + else + true + root = exports ? window root.UIComponent = UIComponent diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee index 0156c8f2..b1d3625a 100644 --- a/pages/test_ui_component.coffee +++ b/pages/test_ui_component.coffee @@ -1,7 +1,2 @@ UIComponentServer.addEventListener "message", (event) -> document.body.innerHTML = event.data - -document.addEventListener "DOMContentLoaded", -> - document.addEventListener "keydown", (event) -> - # Close on any key. - console.log "How do I close myself?" diff --git a/pages/test_ui_component.html b/pages/test_ui_component.html index b8ad7475..06af346c 100644 --- a/pages/test_ui_component.html +++ b/pages/test_ui_component.html @@ -1,6 +1,7 @@ Test + diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee index 240233ed..3879cea0 100644 --- a/pages/ui_component_server.coffee +++ b/pages/ui_component_server.coffee @@ -7,11 +7,13 @@ window.addEventListener "message", (event) -> chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) -> return unless event.data == secret UIComponentServer.portOpen event.ports[0] + window.addEventListener "keydown", (event) -> UIComponentServer.keydownListener event window.removeEventListener "message", currentFunction # Stop listening for message events. UIComponentServer = ownerPagePort: null messageEventListeners: [] + exitOnEsc: true portOpen: (@ownerPagePort) -> @ownerPagePort.onmessage = (event) => @handleMessage event @@ -37,5 +39,12 @@ UIComponentServer = @messageEventListeners = @messageEventListeners.splice listenerIndex, 1 undefined + keydownListener: (event) -> + if @exitOnEsc and KeyboardUtils.isEscape event + @postMessage "hide" + false + else + true + root = exports ? window root.UIComponentServer = UIComponentServer -- cgit v1.2.3 From e5e0ddf24c7975e994b816651beaa4d0cefb94b7 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:07:25 +0000 Subject: Initialise hide/show styles for UIComponent --- content_scripts/ui_component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index f47719e5..ce1af082 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -2,8 +2,8 @@ class UIComponent iframeElement: null iframePort: null messageEventListeners: [] - showStyle: "" - hideStyle: "" + showStyle: "display: block;" + hideStyle: "display: none;" constructor: (iframeUrl, className) -> @iframeElement = document.createElement "iframe" -- cgit v1.2.3 From 4e3ef0b401cfb4682a17a1ee88058ed76d64be20 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:29:09 +0000 Subject: Small changes to UIComponent --- content_scripts/ui_component.coffee | 17 +++++++++++------ content_scripts/vimium_frontend.coffee | 1 - 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index ce1af082..b0e4f71c 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -5,13 +5,16 @@ class UIComponent showStyle: "display: block;" hideStyle: "display: none;" - constructor: (iframeUrl, className) -> + constructor: (iframeUrl, className, showStyle, hideStyle) -> @iframeElement = document.createElement "iframe" @iframeElement.className = className @iframeElement.seamless = "seamless" @iframeElement.src = chrome.runtime.getURL iframeUrl @iframeElement.addEventListener "load", => @openPort() document.documentElement.appendChild @iframeElement + + @setShowStyle showStyle if showStyle? + @setHideStyle hideStyle if showStyle? @hide() @addEventListener "message", handleHideMessage @@ -43,9 +46,7 @@ class UIComponent removeEventListener: (type, listener) -> if type == "message" - listenerIndex = @messageEventListeners.indexOf listener - if listenerIndex > -1 - @messageEventListeners = @messageEventListeners.splice listenerIndex, 1 + @messageEventListeners = @messageEventListeners.filter (f) -> f != listener undefined setHideStyle: (@hideStyle) -> @@ -54,14 +55,18 @@ class UIComponent setShowStyle: (@showStyle) -> @show() if @showing == true + setStyles: (@showStyle = @showStyle, @hideStyle = @hideStyle) -> + if @showing + @show() + else + @hide() + show: -> - return unless @iframeElement? @iframeElement.setAttribute "style", @showStyle @iframeElement.focus() @showing = true hide: -> - return unless @iframeElement? @iframeElement.setAttribute "style", @hideStyle window.focus() @showing = false diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 5c166ff0..a505a84a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -922,7 +922,6 @@ exitFindMode = -> window.showHelpDialog = (html, fid) -> return if (isShowingHelpDialog || !document.body || fid != frameId) - isShowingHelpDialog = true container = document.createElement("div") container.id = "vimiumHelpDialogContainer" -- cgit v1.2.3 From 0433dd338258dc39466593e0bce5fa3253f8d6d5 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:38:25 +0000 Subject: Allow message passing directly from UIComponent.show --- content_scripts/ui_component.coffee | 7 ++++--- content_scripts/vimium_frontend.coffee | 2 -- pages/ui_component_server.coffee | 9 ++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index b0e4f71c..f0593f1b 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -29,9 +29,9 @@ class UIComponent chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) => @iframeElement.contentWindow.postMessage secret, chrome.runtime.getURL(""), [messageChannel.port2] - postMessage: (data) -> @iframePort.postMessage data + postMessage: (message) -> @iframePort.postMessage message - # Execute each event listener on the current event until we get a falsy return value. + # Execute each event listener on the current event until we get a non-null falsy return value. handleMessage: (event) -> for listener in @messageEventListeners retVal = listener.call this, event @@ -61,7 +61,8 @@ class UIComponent else @hide() - show: -> + show: (message) -> + @postMessage message if message? @iframeElement.setAttribute "style", @showStyle @iframeElement.focus() @showing = true diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index a505a84a..beb6c528 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1091,8 +1091,6 @@ Tween = testUIComponent = null testUIComponentSetup = -> testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent" - testUIComponent.setHideStyle "display: none;" - testUIComponent.setShowStyle "display: block;" window.activateTestUIComponent = -> testUIComponent.show() diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee index 3879cea0..7b40be63 100644 --- a/pages/ui_component_server.coffee +++ b/pages/ui_component_server.coffee @@ -18,12 +18,13 @@ UIComponentServer = portOpen: (@ownerPagePort) -> @ownerPagePort.onmessage = (event) => @handleMessage event - postMessage: (data) -> @ownerPagePort.postMessage data + postMessage: (message) -> @ownerPagePort.postMessage message - # Execute each event listener on the current event until we get a falsy return value. + # Execute each event listener on the current event until we get a non-null falsy return value. handleMessage: (event) -> for listener in @messageEventListeners retVal = listener.call this, event + retVal ?= true return false unless retVal true @@ -34,9 +35,7 @@ UIComponentServer = removeEventListener: (type, listener) -> if type == "message" - listenerIndex = @messageEventListeners.indexOf listener - if listenerIndex > -1 - @messageEventListeners = @messageEventListeners.splice listenerIndex, 1 + @messageEventListeners = @messageEventListeners.filter (f) -> f != listener undefined keydownListener: (event) -> -- cgit v1.2.3 From f53af786275c98ce08f51936b165d799a41de024 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:39:44 +0000 Subject: Update test for UIComponent --- content_scripts/vimium_frontend.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index beb6c528..502ecaef 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1093,8 +1093,7 @@ testUIComponentSetup = -> testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent" window.activateTestUIComponent = -> - testUIComponent.show() - testUIComponent.postMessage "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" + testUIComponent.show "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) -- cgit v1.2.3 From 26f37bb0dbda2d833f508db373629408c7ed6e09 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:42:28 +0000 Subject: Remove key handling in UIComponentServer/UIComponent code --- content_scripts/ui_component.coffee | 2 -- pages/ui_component_server.coffee | 8 -------- 2 files changed, 10 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index f0593f1b..12a024e4 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -17,8 +17,6 @@ class UIComponent @setHideStyle hideStyle if showStyle? @hide() - @addEventListener "message", handleHideMessage - # Open a port and pass it to the iframe via window.postMessage. openPort: -> messageChannel = new MessageChannel() diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee index 7b40be63..ccbb9e2b 100644 --- a/pages/ui_component_server.coffee +++ b/pages/ui_component_server.coffee @@ -7,7 +7,6 @@ window.addEventListener "message", (event) -> chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) -> return unless event.data == secret UIComponentServer.portOpen event.ports[0] - window.addEventListener "keydown", (event) -> UIComponentServer.keydownListener event window.removeEventListener "message", currentFunction # Stop listening for message events. UIComponentServer = @@ -38,12 +37,5 @@ UIComponentServer = @messageEventListeners = @messageEventListeners.filter (f) -> f != listener undefined - keydownListener: (event) -> - if @exitOnEsc and KeyboardUtils.isEscape event - @postMessage "hide" - false - else - true - root = exports ? window root.UIComponentServer = UIComponentServer -- cgit v1.2.3 From 4af43db2e65af772787ab55c44604f4760eaee3d Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 10:46:42 +0000 Subject: Update UIComponent test --- content_scripts/vimium_frontend.coffee | 7 +++++++ pages/test_ui_component.coffee | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 502ecaef..a0623d32 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1091,6 +1091,13 @@ Tween = testUIComponent = null testUIComponentSetup = -> testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent" + testUIComponent.addEventListener "message", (event) -> + if event.data == "hide" + @hide() + window.focus() + false + else + true window.activateTestUIComponent = -> testUIComponent.show "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee index b1d3625a..886a3470 100644 --- a/pages/test_ui_component.coffee +++ b/pages/test_ui_component.coffee @@ -1,2 +1,6 @@ UIComponentServer.addEventListener "message", (event) -> document.body.innerHTML = event.data + +window.addEventListener "keydown", (event) -> + if KeyboardUtils.isEscape event + UIComponentServer.postMessage "hide" -- cgit v1.2.3 From a78d49c8a9cac57492f78a90246ce7695cf8e036 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 11:26:30 +0000 Subject: Add a comment clarifying why we no longer use XPath for link hints --- content_scripts/link_hints.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 3a9e2027..b605c2ec 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -135,6 +135,11 @@ LinkHints = elements = document.documentElement.getElementsByTagName "*" visibleElements = [] + # The order of elements here is important; they should appear in the order they are in the DOM, so that + # we can work out which element is on top when multiple elements overlap. Detecting elements in this loop + # is the sensible, efficient way to ensure this happens. + # NOTE(mrmr1993): Our previous method (combined XPath and DOM traversal for jsaction) couldn't provide + # this, so it's necessary to check whether elements are clickable in order, as we do below. for element in elements tagName = element.tagName.toLowerCase() isClickable = false -- cgit v1.2.3 From f38a834cc38d17b086253dea197e6a8945551377 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 11:29:12 +0000 Subject: Add UIComponent.active, so we can specify whether to focus the frame --- content_scripts/ui_component.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 12a024e4..10450778 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -59,10 +59,14 @@ class UIComponent else @hide() + activate: (message) -> + @postMessage message if message? + @show() unless @showing + @iframeElement.focus() + show: (message) -> @postMessage message if message? @iframeElement.setAttribute "style", @showStyle - @iframeElement.focus() @showing = true hide: -> -- cgit v1.2.3 From a6dc63fd7c49926e6ad32174621b32eeb3fd9283 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 11:30:41 +0000 Subject: Update UIComponent test --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index a0623d32..3f898b74 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1100,7 +1100,7 @@ testUIComponentSetup = -> true window.activateTestUIComponent = -> - testUIComponent.show "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" + testUIComponent.activate "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) -- cgit v1.2.3 From da04ee17472177b7ae0474712090d0604db2556e Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 11:46:03 +0000 Subject: Move link hint clickable element detection to its own function --- content_scripts/link_hints.coffee | 127 ++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index b605c2ec..ea4be397 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -124,6 +124,72 @@ LinkHints = marker + # + # Determine whether the element is visible and clickable. If it is, return the element and the rect + # bounding the element in the viewport. + getVisibleClickable: (element) -> + tagName = element.tagName.toLowerCase() + isClickable = false + onlyHasTabIndex = false + + # Insert area elements that provide click functionality to an img. + if tagName == "img" + mapName = element.getAttribute "usemap" + if mapName + imgClientRects = element.getClientRects() + mapName = mapName.replace(/^#/, "").replace("\"", "\\\"") + map = document.querySelector "map[name=\"#{mapName}\"]" + if map and imgClientRects.length > 0 + areas = map.getElementsByTagName "area" + areasAndRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas + visibleElements.push areasAndRects... + + # Check aria properties to see if the element should be ignored. + if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or + element.getAttribute("aria-disabled")?.toLowerCase() in ["", "true"]) + return null # This element should never have a link hint. + + # Check for attributes that make an element clickable regardless of its tagName. + if (element.hasAttribute("onclick") or + element.getAttribute("role")?.toLowerCase() in ["button", "link"] or + element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or + element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) + isClickable = true + + # Check for jsaction event listeners on the element. + if element.hasAttribute "jsaction" + jsactionRules = element.getAttribute("jsaction").split(";") + for jsactionRule in jsactionRules + ruleSplit = jsactionRule.split ":" + isClickable ||= ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none") + + # Check for tagNames which are natively clickable. + switch tagName + when "a" + isClickable = true + when "textarea" + isClickable ||= not element.disabled and not element.readOnly + when "input" + isClickable ||= not (element.getAttribute("type")?.toLowerCase() == "hidden" or + element.disabled or + (element.readOnly and DomUtils.isSelectable element)) + when "button", "select" + isClickable ||= not element.disabled + + # Elements with tabindex are sometimes useful, but usually not. We can treat them as second class + # citizens when it improves UX, so take special note of them. + tabIndexValue = element.getAttribute("tabindex") + tabIndex = if tabIndexValue == "" then 0 else parseInt tabIndexValue + unless isClickable or isNaN(tabIndex) or tabIndex < 0 + isClickable = onlyHasTabIndex = true + + return null unless isClickable # The element isn't clickable. + clientRect = DomUtils.getVisibleClientRect element + if clientRect == null + null + else + {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} + # # Returns all clickable elements that are not hidden and are in the current viewport, along with rectangles # at which (parts of) the elements are displayed. @@ -141,65 +207,8 @@ LinkHints = # NOTE(mrmr1993): Our previous method (combined XPath and DOM traversal for jsaction) couldn't provide # this, so it's necessary to check whether elements are clickable in order, as we do below. for element in elements - tagName = element.tagName.toLowerCase() - isClickable = false - onlyHasTabIndex = false - - # Insert area elements that provide click functionality to an img. - if tagName == "img" - mapName = element.getAttribute "usemap" - if mapName - imgClientRects = element.getClientRects() - mapName = mapName.replace(/^#/, "").replace("\"", "\\\"") - map = document.querySelector "map[name=\"#{mapName}\"]" - if map and imgClientRects.length > 0 - areas = map.getElementsByTagName "area" - areasAndRects = DomUtils.getClientRectsForAreas imgClientRects[0], areas - visibleElements.push areasAndRects... - - # Check aria properties to see if the element should be ignored. - if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or - element.getAttribute("aria-disabled")?.toLowerCase() in ["", "true"]) - continue # No point continuing the loop; this element should never have a link hint - - # Check for attributes that make an element clickable regardless of its tagName. - if (element.hasAttribute("onclick") or - element.getAttribute("role")?.toLowerCase() in ["button", "link"] or - element.getAttribute("class")?.toLowerCase().indexOf("button") >= 0 or - element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) - isClickable = true - - # Check for jsaction event listeners on the element. - if element.hasAttribute "jsaction" - jsactionRules = element.getAttribute("jsaction").split(";") - for jsactionRule in jsactionRules - ruleSplit = jsactionRule.split ":" - isClickable ||= ruleSplit[0] == "click" or (ruleSplit.length == 1 and ruleSplit[0] != "none") - - # Check for tagNames which are natively clickable. - switch tagName - when "a" - isClickable = true - when "textarea" - isClickable ||= not element.disabled and not element.readOnly - when "input" - isClickable ||= not (element.getAttribute("type")?.toLowerCase() == "hidden" or - element.disabled or - (element.readOnly and DomUtils.isSelectable element)) - when "button", "select" - isClickable ||= not element.disabled - - # Elements with tabindex are sometimes useful, but usually not. We can treat them as second class - # citizens when it improves UX, so take special note of them. - tabIndexValue = element.getAttribute("tabindex") - tabIndex = if tabIndexValue == "" then 0 else parseInt tabIndexValue - unless isClickable or isNaN(tabIndex) or tabIndex < 0 - isClickable = onlyHasTabIndex = true - - continue unless isClickable # If the element isn't clickable, do nothing. - clientRect = DomUtils.getVisibleClientRect element - if clientRect != null - visibleElements.push {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} + visibleElement = @getVisibleClickable element + visibleElements.push visibleElement if visibleElement? # TODO(mrmr1993): Consider z-index. z-index affects behviour as follows: # * The document has a local stacking context. -- cgit v1.2.3 From 0524bdc3f76279e8930bfe4b1b42d93e0e9bf6e4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 14:22:53 +0000 Subject: Refactor UIComponent, etc., and demo. - Simplify component API. - Iframe flashes on re-focus. - Probably some other stuff which I've forgotten. --- background_scripts/main.coffee | 5 ++ content_scripts/ui_component.coffee | 79 +++++++++---------------------- content_scripts/vimium_frontend.coffee | 10 ++-- pages/test_ui_component.coffee | 4 +- pages/ui_component_server.coffee | 44 ++++++----------- tests/dom_tests/chrome.coffee | 1 + tests/unit_tests/test_chrome_stubs.coffee | 4 ++ 7 files changed, 56 insertions(+), 91 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 647923c0..b85ea844 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -19,6 +19,11 @@ namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/ selectionChangedHandlers = [] tabLoadedHandlers = {} # tabId -> function() +# A secret, available only within the current instantiation of Vimium. The secret is big, likely unguessable +# in practice, but less than 2^31. +chrome.storage.local.set + vimiumSecret: Math.floor Math.random() * 2000000000 + completionSources = bookmarks: new BookmarkCompleter() history: new HistoryCompleter() diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 10450778..a898d525 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -1,21 +1,19 @@ class UIComponent iframeElement: null iframePort: null - messageEventListeners: [] + showing: true showStyle: "display: block;" hideStyle: "display: none;" - constructor: (iframeUrl, className, showStyle, hideStyle) -> + constructor: (iframeUrl, className, @handleMessage) -> @iframeElement = document.createElement "iframe" @iframeElement.className = className @iframeElement.seamless = "seamless" @iframeElement.src = chrome.runtime.getURL iframeUrl @iframeElement.addEventListener "load", => @openPort() document.documentElement.appendChild @iframeElement - - @setShowStyle showStyle if showStyle? - @setHideStyle hideStyle if showStyle? - @hide() + # Hide iframe, but don't interfere with the focus. + @hide false # Open a port and pass it to the iframe via window.postMessage. openPort: -> @@ -23,63 +21,32 @@ class UIComponent @iframePort = messageChannel.port1 @iframePort.onmessage = (event) => @handleMessage event - # Get iframeMessageSecret so the iframe can determine that our message isn't the page impersonating us. - chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) => + # Get vimiumSecret so the iframe can determine that our message isn't the page impersonating us. + chrome.storage.local.get "vimiumSecret", ({vimiumSecret: secret}) => @iframeElement.contentWindow.postMessage secret, chrome.runtime.getURL(""), [messageChannel.port2] - postMessage: (message) -> @iframePort.postMessage message - - # Execute each event listener on the current event until we get a non-null falsy return value. - handleMessage: (event) -> - for listener in @messageEventListeners - retVal = listener.call this, event - retVal ?= true - return false unless retVal - true - - addEventListener: (type, listener) -> - if type == "message" - @messageEventListeners.push listener - undefined - - removeEventListener: (type, listener) -> - if type == "message" - @messageEventListeners = @messageEventListeners.filter (f) -> f != listener - undefined - - setHideStyle: (@hideStyle) -> - @hide() if @showing == false - - setShowStyle: (@showStyle) -> - @show() if @showing == true - - setStyles: (@showStyle = @showStyle, @hideStyle = @hideStyle) -> - if @showing - @show() - else - @hide() + postMessage: (message) -> + @iframePort.postMessage message activate: (message) -> @postMessage message if message? - @show() unless @showing + if @showing + # NOTE(smblott) Experimental. Not sure this is a great idea. If the iframe was already showing, then + # the user gets no visual feedback when it is re-focused. So flash its border. + borderWas = @iframeElement.style.border + @iframeElement.style.border = '5px solid yellow' + setTimeout((=> @iframeElement.style.border = borderWas), 200) + else + @iframeElement.setAttribute "style", @showStyle + @showing = true @iframeElement.focus() - show: (message) -> - @postMessage message if message? - @iframeElement.setAttribute "style", @showStyle - @showing = true - - hide: -> - @iframeElement.setAttribute "style", @hideStyle - window.focus() - @showing = false - -handleHideMessage = (event) -> - if event.data == "hide" - @hide() - false - else - true + hide: (focusWindow=true)-> + if @showing + @iframeElement.setAttribute "style", @hideStyle + # TODO(smblott) Is window always the right thing to focus, here? + window.focus() if focusWindow + @showing = false root = exports ? window root.UIComponent = UIComponent diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 3f898b74..8fcb16ed 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -1090,17 +1090,17 @@ Tween = testUIComponent = null testUIComponentSetup = -> - testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent" - testUIComponent.addEventListener "message", (event) -> + testUIComponent = new UIComponent "pages/test_ui_component.html", "testUIComponent", (event) -> if event.data == "hide" @hide() window.focus() - false else - true + # ... And we can get data back! + console.log event.data window.activateTestUIComponent = -> - testUIComponent.activate "version: #{chrome.runtime.getManifest().version}; random number: #{Math.random()}" + chrome.storage.local.get "vimiumSecret", ({vimiumSecret: secret}) -> + testUIComponent.activate [chrome.runtime.getManifest().version, secret, Math.random(), ].join "
        " initializePreDomReady() window.addEventListener("DOMContentLoaded", registerFrame) diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee index 886a3470..e140fb14 100644 --- a/pages/test_ui_component.coffee +++ b/pages/test_ui_component.coffee @@ -1,6 +1,8 @@ -UIComponentServer.addEventListener "message", (event) -> +UIComponentServer.registerHandler (event) -> document.body.innerHTML = event.data window.addEventListener "keydown", (event) -> if KeyboardUtils.isEscape event UIComponentServer.postMessage "hide" + else + UIComponentServer.postMessage event.keyCode diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee index ccbb9e2b..8b43095b 100644 --- a/pages/ui_component_server.coffee +++ b/pages/ui_component_server.coffee @@ -1,41 +1,27 @@ -# Register the port recieved from the parent window, and stop listening for messages on the window object. -window.addEventListener "message", (event) -> - return unless event.source == window.parent - currentFunction = arguments.callee - # Check event.data against iframeMessageSecret so we can determine that this message hasn't been spoofed. - chrome.storage.local.get "iframeMessageSecret", ({iframeMessageSecret: secret}) -> - return unless event.data == secret +# Fetch the Vimium secret, register the port recieved from the parent window, and stop listening for messages +# on the window object. vimiumSecret is accessible only within the current instantion of Vimium. So a +# malicious host page trying to register its own port can do no better than guessing. +registerPort = (event) -> + chrome.storage.local.get "vimiumSecret", ({vimiumSecret: secret}) -> + return unless event.source == window.parent and event.data == secret UIComponentServer.portOpen event.ports[0] - window.removeEventListener "message", currentFunction # Stop listening for message events. + window.removeEventListener "message", registerPort + +window.addEventListener "message", registerPort UIComponentServer = ownerPagePort: null - messageEventListeners: [] - exitOnEsc: true + handleMessage: null portOpen: (@ownerPagePort) -> - @ownerPagePort.onmessage = (event) => @handleMessage event - - postMessage: (message) -> @ownerPagePort.postMessage message - - # Execute each event listener on the current event until we get a non-null falsy return value. - handleMessage: (event) -> - for listener in @messageEventListeners - retVal = listener.call this, event - retVal ?= true - return false unless retVal - true + @ownerPagePort.onmessage = (event) => + @handleMessage event if @handleMessage - addEventListener: (type, listener) -> - if type == "message" - @messageEventListeners.push listener - undefined + registerHandler: (@handleMessage) -> - removeEventListener: (type, listener) -> - if type == "message" - @messageEventListeners = @messageEventListeners.filter (f) -> f != listener - undefined + postMessage: (message) -> + @ownerPagePort.postMessage message if @ownerPagePort root = exports ? window root.UIComponentServer = UIComponentServer diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee index 2695ef20..a49a565a 100644 --- a/tests/dom_tests/chrome.coffee +++ b/tests/dom_tests/chrome.coffee @@ -22,4 +22,5 @@ root.chrome = { storage: local: get: -> + set: -> } diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index 80750337..3258bcd6 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -42,6 +42,10 @@ exports.chrome = getAll: () -> true storage: + # chrome.storage.local + local: + set: -> + # chrome.storage.onChanged onChanged: addListener: (func) -> @func = func -- cgit v1.2.3 From c52c0ea57f86c1c5a132819fe85e763db84ce712 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 16:33:23 +0000 Subject: Descriptions for custom search engines. --- background_scripts/completion.coffee | 16 +++++++++++----- background_scripts/settings.coffee | 16 +++++++++++----- pages/options.html | 12 +++++++----- tests/unit_tests/completion_test.coffee | 11 +++++++++-- tests/unit_tests/settings_test.coffee | 13 +++++++------ 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index d62f82fe..4e570313 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -324,11 +324,17 @@ class SearchEngineCompleter searchEngines: {} filter: (queryTerms, onComplete) -> - searchEngineMatch = this.getSearchEngineMatches(queryTerms[0]) + {url: url, description: description} = this.getSearchEngineMatches(queryTerms[0]) suggestions = [] - if searchEngineMatch - searchEngineMatch = searchEngineMatch.replace(/%s/g, Utils.createSearchQuery queryTerms[1..]) - suggestion = new Suggestion(queryTerms, "search", searchEngineMatch, queryTerms[0] + ": " + queryTerms[1..].join(" "), @computeRelevancy) + if url + url = url.replace(/%s/g, Utils.createSearchQuery queryTerms[1..]) + if description + type = description + query = queryTerms[1..].join " " + else + type = "search" + query = queryTerms[0] + ": " + queryTerms[1..].join(" ") + suggestion = new Suggestion(queryTerms, type, url, query, @computeRelevancy) suggestions.push(suggestion) onComplete(suggestions) @@ -338,7 +344,7 @@ class SearchEngineCompleter this.searchEngines = root.Settings.getSearchEngines() getSearchEngineMatches: (queryTerm) -> - this.searchEngines[queryTerm] + this.searchEngines[queryTerm] || {} # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top # 10. Queries from the vomnibar frontend script come through a multi completer. diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index e90bc1f8..3ff74749 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -46,13 +46,19 @@ root.Settings = Settings = # this is a map that we use to store our search engines for use. searchEnginesMap: {} - # this parses the search engines settings and clears the old searchEngines and sets the new one + # Parse the custom search engines setting and cache it. parseSearchEngines: (searchEnginesText) -> @searchEnginesMap = {} - # find the split pairs by first splitting by line then splitting on the first `: ` - split_pairs = ( pair.split( /: (.+)/, 2) for pair in searchEnginesText.split( /\n/ ) when pair[0] != "#" ) - @searchEnginesMap[a[0]] = a[1] for a in split_pairs - @searchEnginesMap + for line in searchEnginesText.split /\n/ + tokens = line.trim().split /\s+/ + continue if tokens.length < 2 or tokens[0].startsWith('"') or tokens[0].startsWith("#") + keywords = tokens[0].split ":" + continue unless keywords.length == 2 and not keywords[1] # So, like: [ "w", "" ]. + @searchEnginesMap[keywords[0]] = + url: tokens[1] + description: tokens[2..].join(" ") + + # Fetch the search-engine map, building it if necessary. getSearchEngines: -> this.parseSearchEngines(@get("searchEngines") || "") if Object.keys(@searchEnginesMap).length == 0 @searchEnginesMap diff --git a/pages/options.html b/pages/options.html index 8e685304..e765334c 100644 --- a/pages/options.html +++ b/pages/options.html @@ -300,11 +300,13 @@ unmapAll
        - This adds search-engine shortcuts to the Vomnibar.

        - The format is:
        -
        your-keyword: http://the-site.com/?q=%s
        - %s will be replaced with your search terms.
        - Lines which start with "#" are comments. + Add search-engine shortcuts to the Vomnibar. Format:
        +
        +a: http://a.com/?q=%s
        +b: http://b.com/?q=%s description
        +" this is a comment
        +# this is also a comment
        + %s is replaced with the search terms.
        diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee index e4966016..b7b73cc2 100644 --- a/tests/unit_tests/completion_test.coffee +++ b/tests/unit_tests/completion_test.coffee @@ -233,17 +233,24 @@ context "tab completer", context "search engines", setup -> - searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s" + searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description" Settings.set 'searchEngines', searchEngines @completer = new SearchEngineCompleter() # note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors # workaround is below, would be good for someone that understands the testing system better than me to improve @completer.searchEngines = Settings.getSearchEngines() - should "return search engine suggestion", -> + should "return search engine suggestion without description", -> results = filterCompleter(@completer, ["foo", "hello"]) assert.arrayEqual ["bar?q=hello"], results.map (result) -> result.url assert.arrayEqual ["foo: hello"], results.map (result) -> result.title + assert.arrayEqual ["search"], results.map (result) -> result.type + + should "return search engine suggestion with description", -> + results = filterCompleter(@completer, ["baz", "hello"]) + assert.arrayEqual ["qux?q=hello"], results.map (result) -> result.url + assert.arrayEqual ["hello"], results.map (result) -> result.title + assert.arrayEqual ["baz description"], results.map (result) -> result.type context "suggestions", should "escape html in page titles", -> diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 4625457b..afe862a4 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -70,14 +70,15 @@ context "settings", chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) } assert.equal message, Sync.message - should "set search engines, retrieve them correctly and check that it has been parsed correctly", -> - searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s" - parsedSearchEngines = {"foo": "bar?q=%s", "baz": "qux?q=%s"} + should "set search engines, retrieve them correctly and check that they have been parsed correctly", -> + searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s baz description" Settings.set 'searchEngines', searchEngines - assert.equal(searchEngines, Settings.get('searchEngines')) result = Settings.getSearchEngines() - assert.isTrue(parsedSearchEngines["foo"] == result["foo"] && - parsedSearchEngines["baz"] == result["baz"] && Object.keys(result).length == 2) + assert.equal Object.keys(result).length, 2 + assert.equal "bar?q=%s", result["foo"].url + assert.isFalse result["foo"].description + assert.equal "qux?q=%s", result["baz"].url + assert.equal "baz description", result["baz"].description should "sync a key which is not a known setting (without crashing)", -> chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") } -- cgit v1.2.3 From 7499675455941251eaa69c93e7c66bfb1c6ae35c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 17:08:39 +0000 Subject: Clearer handling of @showing in UI component. --- content_scripts/ui_component.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index a898d525..d89f0cc8 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -1,7 +1,7 @@ class UIComponent iframeElement: null iframePort: null - showing: true + showing: null showStyle: "display: block;" hideStyle: "display: none;" @@ -12,7 +12,8 @@ class UIComponent @iframeElement.src = chrome.runtime.getURL iframeUrl @iframeElement.addEventListener "load", => @openPort() document.documentElement.appendChild @iframeElement - # Hide iframe, but don't interfere with the focus. + @showing = true # The iframe is visible now. + # Hide the iframe, but don't interfere with the focus. @hide false # Open a port and pass it to the iframe via window.postMessage. @@ -44,7 +45,6 @@ class UIComponent hide: (focusWindow=true)-> if @showing @iframeElement.setAttribute "style", @hideStyle - # TODO(smblott) Is window always the right thing to focus, here? window.focus() if focusWindow @showing = false -- cgit v1.2.3 From 2dc855abaeeda8ad74c3179f7224858860672338 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 29 Dec 2014 17:32:03 +0000 Subject: Remove UI component demo. --- background_scripts/commands.coffee | 3 --- content_scripts/vimium_frontend.coffee | 1 + pages/test_ui_component.coffee | 8 -------- pages/test_ui_component.html | 11 ----------- 4 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 pages/test_ui_component.coffee delete mode 100644 pages/test_ui_component.html diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index 63b870cc..585ef572 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -91,7 +91,6 @@ Commands = commandGroups: pageNavigation: ["scrollDown", - "activateTestUIComponent", "scrollUp", "scrollLeft", "scrollRight", @@ -253,7 +252,6 @@ defaultKeyMappings = "m": "Marks.activateCreateMode" "`": "Marks.activateGotoMode" - "D": "activateTestUIComponent" # This is a mapping of: commandIdentifier => [description, options]. @@ -265,7 +263,6 @@ commandDescriptions = scrollUp: ["Scroll up"] scrollLeft: ["Scroll left"] scrollRight: ["Scroll right"] - activateTestUIComponent: ["UI component test"] scrollToTop: ["Scroll to the top of the page", { noRepeat: true }] scrollToBottom: ["Scroll to the bottom of the page", { noRepeat: true }] diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 114786e8..2de08c39 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -198,6 +198,7 @@ window.addEventListener "focus", -> # initializeOnDomReady = -> enterInsertModeIfElementIsFocused() if isEnabledForUrl + # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) CursorHider.init() diff --git a/pages/test_ui_component.coffee b/pages/test_ui_component.coffee deleted file mode 100644 index e140fb14..00000000 --- a/pages/test_ui_component.coffee +++ /dev/null @@ -1,8 +0,0 @@ -UIComponentServer.registerHandler (event) -> - document.body.innerHTML = event.data - -window.addEventListener "keydown", (event) -> - if KeyboardUtils.isEscape event - UIComponentServer.postMessage "hide" - else - UIComponentServer.postMessage event.keyCode diff --git a/pages/test_ui_component.html b/pages/test_ui_component.html deleted file mode 100644 index 06af346c..00000000 --- a/pages/test_ui_component.html +++ /dev/null @@ -1,11 +0,0 @@ - - - Test - - - - - - - - -- cgit v1.2.3 From ecdf878c890bcc4ac67d2bb147dcca2e5c20dd27 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 18:01:42 +0000 Subject: Return an array from getVisibleClickable, to restore img map support --- content_scripts/link_hints.coffee | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index ea4be397..70e6a626 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -131,6 +131,7 @@ LinkHints = tagName = element.tagName.toLowerCase() isClickable = false onlyHasTabIndex = false + visibleElements = [] # Insert area elements that provide click functionality to an img. if tagName == "img" @@ -183,12 +184,12 @@ LinkHints = unless isClickable or isNaN(tabIndex) or tabIndex < 0 isClickable = onlyHasTabIndex = true - return null unless isClickable # The element isn't clickable. - clientRect = DomUtils.getVisibleClientRect element - if clientRect == null - null - else - {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} + if isClickable + clientRect = DomUtils.getVisibleClientRect element + if clientRect != null + visibleElements.push {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} + + visibleElements # # Returns all clickable elements that are not hidden and are in the current viewport, along with rectangles @@ -208,7 +209,7 @@ LinkHints = # this, so it's necessary to check whether elements are clickable in order, as we do below. for element in elements visibleElement = @getVisibleClickable element - visibleElements.push visibleElement if visibleElement? + visibleElements.push visibleElement... # TODO(mrmr1993): Consider z-index. z-index affects behviour as follows: # * The document has a local stacking context. -- cgit v1.2.3 From 57418a9ad6104c487b67fcfd27ec8503858e5a14 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 18:55:06 +0000 Subject: Use UIComponent for Vomnibar iframe --- content_scripts/ui_component.coffee | 2 +- content_scripts/vimium_frontend.coffee | 3 +- content_scripts/vomnibar.coffee | 42 +++++++---------------- pages/vomnibar.coffee | 61 +++++++++------------------------- pages/vomnibar.html | 1 + 5 files changed, 30 insertions(+), 79 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index d89f0cc8..696cb42c 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -42,7 +42,7 @@ class UIComponent @showing = true @iframeElement.focus() - hide: (focusWindow=true)-> + hide: (focusWindow = true)-> if @showing @iframeElement.setAttribute "style", @hideStyle window.focus() if focusWindow diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 2de08c39..ae275f0c 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -139,8 +139,6 @@ initializePreDomReady = -> getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue - vomnibarShow: -> Vomnibar.show() - vomnibarClose: -> Vomnibar.close() chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those @@ -202,6 +200,7 @@ initializeOnDomReady = -> # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) CursorHider.init() + Vomnibar.init() registerFrame = -> # Don't register frameset containers; focusing them is no use. diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 10f75652..6b82d31c 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -2,7 +2,7 @@ # This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar. # Vomnibar = - vomnibarElement: null + vomnibarUI: null activate: -> @open {completer:"omni"} activateInNewTab: -> @open { @@ -35,41 +35,23 @@ Vomnibar = newTab: true } + init: -> + unless @vomnibarUI? + @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", @handleMessage.bind this + + handleMessage: (event) -> + if event.data == "hide" + @hide() + + # This function opens the vomnibar. It accepts options, a map with the values: # completer - The completer to fetch results from. # query - Optional. Text to prefill the Vomnibar with. # selectFirst - Optional, boolean. Whether to select the first entry. # newTab - Optional, boolean. Whether to open the result in a new tab. - open: (options) -> - unless @vomnibarElement? - @vomnibarElement = document.createElement "iframe" - @vomnibarElement.className = "vomnibarFrame" - @vomnibarElement.seamless = "seamless" - @hide() - - options.frameId = frameId - - optionStrings = [] - for option of options - if typeof options[option] == "boolean" - optionStrings.push option if options[option] - else - optionStrings.push "#{option}=#{escape(options[option])}" - - @vomnibarElement.src = "#{chrome.runtime.getURL "pages/vomnibar.html"}?#{optionStrings.join "&"}" - document.documentElement.appendChild @vomnibarElement - - @vomnibarElement.focus() - - close: -> - @hide() - @vomnibarElement?.remove() - - show: -> - @vomnibarElement?.style.display = "block" + open: (options) -> @vomnibarUI.activate options - hide: -> - @vomnibarElement?.style.display = "none" + hide: -> @vomnibarUI?.hide() root = exports ? window root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 2b6980f7..2d5fdd1c 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -16,25 +16,13 @@ Vomnibar = # # Activate the Vomnibox. # - activate: (params = "") -> + activate: (userOptions) -> options = completer: "omni" query: null - frameId: -1 - - booleanOptions = ["selectFirst", "newTab"] - - # Convert options/params in URL to options object. - params - .split(/[\?&]/) - .map((option) -> - [name, value] = option.split "=" - options[name] = if value? then unescape(value) else true - ) - - # Set boolean options. - for option in booleanOptions - options[option] = option of options and options[option] != "false" + newTab: false + selectFirst: false + extend options, userOptions options.refreshInterval = switch options.completer when "omni" then 100 @@ -47,11 +35,8 @@ Vomnibar = @vomnibarUI.setCompleter(completer) @vomnibarUI.setRefreshInterval(options.refreshInterval) @vomnibarUI.setForceNewTab(options.newTab) - @vomnibarUI.setFrameId(options.frameId) - @vomnibarUI.show() - if (options.query) - @vomnibarUI.setQuery(options.query) - @vomnibarUI.update() + @vomnibarUI.setQuery(options.query) if options.query + @vomnibarUI.update() class VomnibarUI constructor: -> @@ -71,30 +56,14 @@ class VomnibarUI setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab - setFrameId: (frameId) -> @frameId = frameId - - show: -> - @box.style.display = "block" - @input.focus() - @input.addEventListener "keydown", @onKeydown - - chrome.runtime.sendMessage - handler: "echo" - name: "vomnibarShow" - frameId: @frameId - hide: -> - @box.style.display = "none" - @completionList.style.display = "none" @input.blur() - @input.removeEventListener "keydown", @onKeydown window.parent.focus() - chrome.runtime.sendMessage - handler: "echo" - name: "vomnibarClose" - frameId: @frameId + UIComponentServer.postMessage "hide" + @reset() reset: -> + @completionList.style.display = "none" @input.value = "" @updateTimer = null @completions = [] @@ -188,7 +157,7 @@ class VomnibarUI @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) @updateSelection() - update: (updateSynchronously, callback) -> + update: (updateSynchronously, callback) => if (updateSynchronously) # cancel scheduled update if (@updateTimer != null) @@ -205,11 +174,14 @@ class VomnibarUI @updateTimer = null @refreshInterval) + @input.focus() + initDom: -> @box = document.getElementById("vomnibar") @input = @box.querySelector("input") - @input.addEventListener "input", => @update() + @input.addEventListener "input", @update + @input.addEventListener "keydown", @onKeydown @completionList = @box.querySelector("ul") @completionList.style.display = "none" @@ -259,10 +231,7 @@ extend BackgroundCompleter, switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) -initializeOnDomReady = -> - Vomnibar.activate document.location.search - -window.addEventListener "DOMContentLoaded", initializeOnDomReady +UIComponentServer.registerHandler (event) -> Vomnibar.activate event.data root = exports ? window root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.html b/pages/vomnibar.html index 6cba99e6..2ca463d0 100644 --- a/pages/vomnibar.html +++ b/pages/vomnibar.html @@ -6,6 +6,7 @@ + -- cgit v1.2.3 From b5e9dcb5f87391c0da937517182889eb47ec8f0a Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Mon, 29 Dec 2014 19:26:26 +0000 Subject: Fix tests for UIComponent Vomnibar iframe --- tests/dom_tests/dom_tests.html | 17 +---------------- tests/dom_tests/vomnibar_test.coffee | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index 863c5611..7b154d24 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -36,13 +36,9 @@ - + - @@ -57,16 +53,5 @@

        Vimium Tests

        - - -
        -
        - -
        -
          -
          - diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee index f660f96b..0e02bb7b 100644 --- a/tests/dom_tests/vomnibar_test.coffee +++ b/tests/dom_tests/vomnibar_test.coffee @@ -1,19 +1,34 @@ +vomnibarFrame = null + context "Keep selection within bounds", setup -> @completions = [] - oldGetCompleter = Vomnibar.getCompleter.bind Vomnibar - stub Vomnibar, 'getCompleter', (name) => + + vomnibarFrame = Vomnibar.vomnibarUI.iframeElement.contentWindow + + # The Vomnibar frame is dynamically injected, so inject our stubs here. + vomnibarFrame.Function::bind = Function::bind + vomnibarFrame.chrome = chrome + + oldGetCompleter = vomnibarFrame.Vomnibar.getCompleter.bind vomnibarFrame.Vomnibar + stub vomnibarFrame.Vomnibar, 'getCompleter', (name) => completer = oldGetCompleter name stub completer, 'filter', (query, callback) => callback(@completions) completer + # Shoulda.js doesn't support async tests, so we have to hack around. + stub Vomnibar.vomnibarUI, "postMessage", (data) -> + vomnibarFrame.UIComponentServer.handleMessage {data} + stub vomnibarFrame.UIComponentServer, "postMessage", (data) -> + UIComponent.handleMessage {data} + tearDown -> Vomnibar.vomnibarUI.hide() should "set selection to position -1 for omni completion by default", -> Vomnibar.activate() - ui = Vomnibar.vomnibarUI + ui = vomnibarFrame.Vomnibar.vomnibarUI @completions = [] ui.update(true) @@ -28,8 +43,8 @@ context "Keep selection within bounds", assert.equal -1, ui.selection should "set selection to position 0 for bookmark completion if possible", -> - Vomnibar.activate "completer=bookmark&selectFirst" - ui = Vomnibar.vomnibarUI + Vomnibar.activateBookmarks() + ui = vomnibarFrame.Vomnibar.vomnibarUI @completions = [] ui.update(true) @@ -45,7 +60,7 @@ context "Keep selection within bounds", should "keep selection within bounds", -> Vomnibar.activate() - ui = Vomnibar.vomnibarUI + ui = vomnibarFrame.Vomnibar.vomnibarUI @completions = [] ui.update(true) -- cgit v1.2.3 From 774915f3967655ab800cc3c1ac73f0746618d3de Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 06:07:20 +0000 Subject: Minor changes to vomnibar-in-iframe. From top to bottom on the diff: - The echo handler on the background page is no longer required. - Simplify/refactor vomnibarUI message handler. - Initialise vomnibar query to "" (rather than null) and simplify. - No need to focus parent window when vomnibar closes; that's handled by the iframe framework. Also no need to blur. --- background_scripts/main.coffee | 7 ------- content_scripts/vomnibar.coffee | 10 ++-------- pages/vomnibar.coffee | 6 ++---- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index d01a3f6c..44ab5bac 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -622,12 +622,6 @@ handleFrameFocused = (request, sender) -> frameIdsForTab[tabId] = [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] -# Send message back to the tab unchanged. This allows different frames from the same tab to message eachother -# while avoiding security concerns such as eavesdropping or message spoofing. -echo = (request, sender) -> - delete request.handler # No need to send this information - chrome.tabs.sendMessage(sender.tab.id, request) - # Port handler mapping portHandlers = keyDown: handleKeyDown, @@ -654,7 +648,6 @@ sendRequestHandlers = refreshCompleter: refreshCompleter createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) - echo: echo # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 6b82d31c..0d5197a5 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -37,12 +37,8 @@ Vomnibar = init: -> unless @vomnibarUI? - @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", @handleMessage.bind this - - handleMessage: (event) -> - if event.data == "hide" - @hide() - + @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", => + @vomnibarUI.hide() # This function opens the vomnibar. It accepts options, a map with the values: # completer - The completer to fetch results from. @@ -51,7 +47,5 @@ Vomnibar = # newTab - Optional, boolean. Whether to open the result in a new tab. open: (options) -> @vomnibarUI.activate options - hide: -> @vomnibarUI?.hide() - root = exports ? window root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 2d5fdd1c..170f1b42 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -19,7 +19,7 @@ Vomnibar = activate: (userOptions) -> options = completer: "omni" - query: null + query: "" newTab: false selectFirst: false extend options, userOptions @@ -35,7 +35,7 @@ Vomnibar = @vomnibarUI.setCompleter(completer) @vomnibarUI.setRefreshInterval(options.refreshInterval) @vomnibarUI.setForceNewTab(options.newTab) - @vomnibarUI.setQuery(options.query) if options.query + @vomnibarUI.setQuery(options.query) @vomnibarUI.update() class VomnibarUI @@ -57,8 +57,6 @@ class VomnibarUI setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab hide: -> - @input.blur() - window.parent.focus() UIComponentServer.postMessage "hide" @reset() -- cgit v1.2.3 From 74b5c1a9bb54bbc2a2c9d30925d514e02a5515f7 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 07:16:45 +0000 Subject: Add description to default search engines settings. --- background_scripts/settings.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index 3ff74749..2fc3b43d 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -111,7 +111,7 @@ root.Settings = Settings = # default/fall back search engine searchUrl: "http://www.google.com/search?q=" # put in an example search engine - searchEngines: "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s" + searchEngines: "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s wikipedia" newTabUrl: "chrome://newtab" settingsVersion: Utils.getCurrentVersion() -- cgit v1.2.3 From 5a8000da930397e12a1dc9568ecab25c8aea7d38 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 12:36:41 +0000 Subject: Vomnibar; completion list is initially hidden. --- pages/vomnibar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/vomnibar.css b/pages/vomnibar.css index c4bc0008..2042a6c4 100644 --- a/pages/vomnibar.css +++ b/pages/vomnibar.css @@ -3,7 +3,7 @@ #vomnibar ol, #vomnibar ul { list-style: none; - display: block; + display: none; } #vomnibar { -- cgit v1.2.3 From ff7a66a205516a7efcce3be47f82a4c0dde504a6 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 30 Dec 2014 14:13:18 +0000 Subject: Explicitly fall back to hide Vomnibar completions --- pages/vomnibar.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 170f1b42..78c954cd 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -61,7 +61,7 @@ class VomnibarUI @reset() reset: -> - @completionList.style.display = "none" + @completionList.style.display = "" @input.value = "" @updateTimer = null @completions = [] @@ -151,7 +151,7 @@ class VomnibarUI populateUiWithCompletions: (completions) -> # update completion list with the new data @completionList.innerHTML = completions.map((completion) -> "
        • #{completion.html}
        • ").join("") - @completionList.style.display = if completions.length > 0 then "block" else "none" + @completionList.style.display = if completions.length > 0 then "block" else "" @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) @updateSelection() @@ -181,7 +181,7 @@ class VomnibarUI @input.addEventListener "input", @update @input.addEventListener "keydown", @onKeydown @completionList = @box.querySelector("ul") - @completionList.style.display = "none" + @completionList.style.display = "" window.addEventListener "focus", => @input.focus() -- cgit v1.2.3 From a4a591156f451c1d360530fce6674189f384b452 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 14:35:19 +0000 Subject: Vomnibar; reinstate test on event.data. Accidentally dropped in 1bc415536fe45ad40ac37282ea54644361ec73f7. --- content_scripts/vomnibar.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 0d5197a5..6381fd7f 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -37,8 +37,8 @@ Vomnibar = init: -> unless @vomnibarUI? - @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", => - @vomnibarUI.hide() + @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", (event) => + @vomnibarUI.hide() if event.data == "hide" # This function opens the vomnibar. It accepts options, a map with the values: # completer - The completer to fetch results from. -- cgit v1.2.3 From c3df7699527f88c660e0d61fafdd1ad334236d77 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 15:09:53 +0000 Subject: Minor changes to link-hint code. --- content_scripts/link_hints.coffee | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 70e6a626..8d476529 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -125,8 +125,10 @@ LinkHints = marker # - # Determine whether the element is visible and clickable. If it is, return the element and the rect - # bounding the element in the viewport. + # Determine whether the element is visible and clickable. If it is, return the element and the rect bounding + # the element in the viewport. There may be more than one part of element which is clickable (for example, + # if it's an image), therefore we return a list of element/rect pairs. + # getVisibleClickable: (element) -> tagName = element.tagName.toLowerCase() isClickable = false @@ -148,7 +150,7 @@ LinkHints = # Check aria properties to see if the element should be ignored. if (element.getAttribute("aria-hidden")?.toLowerCase() in ["", "true"] or element.getAttribute("aria-disabled")?.toLowerCase() in ["", "true"]) - return null # This element should never have a link hint. + return [] # This element should never have a link hint. # Check for attributes that make an element clickable regardless of its tagName. if (element.hasAttribute("onclick") or @@ -187,7 +189,7 @@ LinkHints = if isClickable clientRect = DomUtils.getVisibleClientRect element if clientRect != null - visibleElements.push {element: element, rect: clientRect, onlyHasTabIndex: onlyHasTabIndex} + visibleElements.push {element: element, rect: clientRect, secondClassCitizen: onlyHasTabIndex} visibleElements @@ -236,10 +238,11 @@ LinkHints = nonOverlappingElements.push {element: visibleElement.element, rect: rects[0]} else # Every part of the element is covered by some other element, so just insert the whole element's - # rect. Except for elements with tabIndex set; these are often more trouble than they're worth. + # rect. Except for elements with tabIndex set (second class citizens); these are often more trouble + # than they're worth. # TODO(mrmr1993): This is probably the wrong thing to do, but we don't want to stop being able to # click some elements that we could click before. - nonOverlappingElements.push visibleElement unless visibleElement.onlyHasTabIndex + nonOverlappingElements.push visibleElement unless visibleElement.secondClassCitizen nonOverlappingElements -- cgit v1.2.3 From f946d23125a80233799564b57253ace2c44b8994 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 30 Dec 2014 15:20:49 +0000 Subject: Reinstate UIComponent.show --- content_scripts/ui_component.coffee | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 696cb42c..8b229725 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -38,15 +38,18 @@ class UIComponent @iframeElement.style.border = '5px solid yellow' setTimeout((=> @iframeElement.style.border = borderWas), 200) else - @iframeElement.setAttribute "style", @showStyle - @showing = true + @show() @iframeElement.focus() + show: (message) -> + @postMessage message if message? + @iframeElement.setAttribute "style", @showStyle + @showing = true + hide: (focusWindow = true)-> - if @showing - @iframeElement.setAttribute "style", @hideStyle - window.focus() if focusWindow - @showing = false + @iframeElement.setAttribute "style", @hideStyle + window.focus() if focusWindow + @showing = false root = exports ? window root.UIComponent = UIComponent -- cgit v1.2.3 From 7e6b2c5a8439cf8c1e861e3f596915a75ecb9644 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 30 Dec 2014 15:31:30 +0000 Subject: Use classes and a stylesheet for UIComponen --- content_scripts/ui_component.coffee | 13 ++++++------- content_scripts/vimium.css | 13 +++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 8b229725..c4ed3bf6 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -2,8 +2,6 @@ class UIComponent iframeElement: null iframePort: null showing: null - showStyle: "display: block;" - hideStyle: "display: none;" constructor: (iframeUrl, className, @handleMessage) -> @iframeElement = document.createElement "iframe" @@ -34,20 +32,21 @@ class UIComponent if @showing # NOTE(smblott) Experimental. Not sure this is a great idea. If the iframe was already showing, then # the user gets no visual feedback when it is re-focused. So flash its border. - borderWas = @iframeElement.style.border - @iframeElement.style.border = '5px solid yellow' - setTimeout((=> @iframeElement.style.border = borderWas), 200) + @iframeElement.classList.add "vimiumUIComponentReactivated" + setTimeout((=> @iframeElement.classList.remove "vimiumUIComponentReactivated"), 200) else @show() @iframeElement.focus() show: (message) -> @postMessage message if message? - @iframeElement.setAttribute "style", @showStyle + @iframeElement.classList.remove "vimiumUIComponentHidden" + @iframeElement.classList.add "vimiumUIComponentShowing" @showing = true hide: (focusWindow = true)-> - @iframeElement.setAttribute "style", @hideStyle + @iframeElement.classList.remove "vimiumUIComponentShowing" + @iframeElement.classList.add "vimiumUIComponentHidden" window.focus() if focusWindow @showing = false diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index f582824a..ec1a09e6 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -302,3 +302,16 @@ div#vimiumFlash { position: absolute; z-index: 2147483648; } + +/* UIComponent CSS */ +iframe.vimiumUIComponentHidden { + display: none; +} + +iframe.vimiumUIComponentVisible { + display: block; +} + +iframe.vimiumUIComponentReactivated { + border: 5px solid yellow; +} -- cgit v1.2.3 From 8f998f5b4cd1d8600b62ae7faac8afb91c4d2dab Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 15:59:26 +0000 Subject: Update comment in getVisibleClickable. --- content_scripts/link_hints.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 8d476529..9f21d109 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -125,9 +125,9 @@ LinkHints = marker # - # Determine whether the element is visible and clickable. If it is, return the element and the rect bounding - # the element in the viewport. There may be more than one part of element which is clickable (for example, - # if it's an image), therefore we return a list of element/rect pairs. + # Determine whether the element is visible and clickable. If it is, find the rect bounding the element in + # the viewport. There may be more than one part of element which is clickable (for example, if it's an + # image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty). # getVisibleClickable: (element) -> tagName = element.tagName.toLowerCase() -- cgit v1.2.3 From 094753c5a536ebacdaa3f811d198595ef2c67d24 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 30 Dec 2014 16:57:27 +0000 Subject: Delay recognising query as search-engine query. --- background_scripts/completion.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 4e570313..5cab54ed 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -324,7 +324,7 @@ class SearchEngineCompleter searchEngines: {} filter: (queryTerms, onComplete) -> - {url: url, description: description} = this.getSearchEngineMatches(queryTerms[0]) + {url: url, description: description} = @getSearchEngineMatches queryTerms suggestions = [] if url url = url.replace(/%s/g, Utils.createSearchQuery queryTerms[1..]) @@ -343,8 +343,8 @@ class SearchEngineCompleter refresh: -> this.searchEngines = root.Settings.getSearchEngines() - getSearchEngineMatches: (queryTerm) -> - this.searchEngines[queryTerm] || {} + getSearchEngineMatches: (queryTerms) -> + if 1 < queryTerms.length and engine = @searchEngines[queryTerms[0]] then engine else {} # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top # 10. Queries from the vomnibar frontend script come through a multi completer. -- cgit v1.2.3 From 877bf98618ba4f8c2dfdb6a82459a98816062112 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 31 Dec 2014 08:58:06 +0000 Subject: Simplify search engine logic. --- background_scripts/completion.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 5cab54ed..d6402019 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -344,7 +344,7 @@ class SearchEngineCompleter this.searchEngines = root.Settings.getSearchEngines() getSearchEngineMatches: (queryTerms) -> - if 1 < queryTerms.length and engine = @searchEngines[queryTerms[0]] then engine else {} + (1 < queryTerms.length and @searchEngines[queryTerms[0]]) or {} # A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top # 10. Queries from the vomnibar frontend script come through a multi completer. -- cgit v1.2.3 From 4a00f76f2ad3d0ec8cdb70c7e45f561d5a73ff27 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 31 Dec 2014 15:01:56 +0000 Subject: Fix vomnibar flicker after tabs mode. In tabs mode, the vomnibar is pre-populated when the query is empty. If, as part of hiding the vomnibar, we reset it, it becomes populated again, so the display style is reset from "none" to "block". Therefore, the completion list is briefly visible when the vomnibar is later reactivated. Solution: - Do not run `@update()` from `@reset()`. --- pages/vomnibar.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 78c954cd..0ade7f0e 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -51,6 +51,7 @@ class VomnibarUI setCompleter: (completer) -> @completer = completer @reset() + @update(true) setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval @@ -66,7 +67,6 @@ class VomnibarUI @updateTimer = null @completions = [] @selection = @initialSelectionValue - @update(true) updateSelection: -> # We have taken the option to add some global state here (previousCompletionType) to tell if a search -- cgit v1.2.3 From fc2201b996e47ca06090e10e4ebfcd9f4b345fde Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 06:43:06 +0000 Subject: Catch undefined reference to handledKeydownEvents. --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ae275f0c..351a2690 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -463,7 +463,7 @@ onKeydown = (event) -> event.srcElement.blur() exitInsertMode() DomUtils.suppressEvent event - handledKeydownEvents.push event + KeydownEvents.push event else if (findMode) if (KeyboardUtils.isEscape(event)) -- cgit v1.2.3