aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
authorStephen Blott2015-01-06 05:39:25 +0000
committerStephen Blott2015-01-06 05:39:25 +0000
commit30dee76c6ab1de9e2a62701dacffc29fa5be0866 (patch)
tree1aab7586b612a92222a4cfe85f4d4f5173e236bc /content_scripts
parent3620fec662ab89bd4f7827e66deec49ff4d11b8e (diff)
parentfc2201b996e47ca06090e10e4ebfcd9f4b345fde (diff)
downloadvimium-30dee76c6ab1de9e2a62701dacffc29fa5be0866.tar.bz2
Merge pull request #1407 from smblott-github/post-1.46
Merge post-1.46 in its entirety
Diffstat (limited to 'content_scripts')
-rw-r--r--content_scripts/link_hints.coffee158
-rw-r--r--content_scripts/scroller.coffee25
-rw-r--r--content_scripts/ui_component.coffee54
-rw-r--r--content_scripts/vimium.css135
-rw-r--r--content_scripts/vimium_frontend.coffee67
-rw-r--r--content_scripts/vomnibar.coffee286
6 files changed, 317 insertions, 408 deletions
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 24bd7126..9f21d109 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)
@@ -136,45 +125,128 @@ 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.
+ # 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).
#
- getVisibleClickableElements: ->
- resultSet = DomUtils.evaluateXPath(@clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
-
+ getVisibleClickable: (element) ->
+ tagName = element.tagName.toLowerCase()
+ isClickable = false
+ onlyHasTabIndex = false
visibleElements = []
- # Find all visible clickable elements.
- for i in [0...resultSet.snapshotLength] by 1
- element = resultSet.snapshotItem(i)
- 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})
+ # 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 [] # 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
+
+ if isClickable
+ clientRect = DomUtils.getVisibleClientRect element
+ if clientRect != null
+ visibleElements.push {element: element, rect: clientRect, secondClassCitizen: onlyHasTabIndex}
visibleElements
#
+ # 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 "*"
+ 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
+ visibleElement = @getVisibleClickable element
+ visibleElements.push visibleElement...
+
+ # 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 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]
+ for {rect: negativeRect} in visibleElements
+ # 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
+ # 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 (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.secondClassCitizen
+
+ nonOverlappingElements
+
+ #
# Handles shift and esc keys. The other keys are passed to getMarkerMatcher().matchHintsByKey.
#
onKeyDownInMode: (hintMarkers, event) ->
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 09470158..889dc042 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
@@ -63,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,
@@ -80,6 +84,19 @@ 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 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) or doesScroll(element, "y", -1, 1)
+ element
+ else
+ 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) ->
# 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
@@ -202,7 +219,7 @@ Scroller =
window.scrollBy(0, amount)
return
- activatedElement ||= document.body
+ activatedElement ||= document.body and firstScrollableElement()
return unless activatedElement
# Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth,
@@ -213,8 +230,8 @@ Scroller =
CoreScroller.scroll element, direction, elementAmount
scrollTo: (direction, pos) ->
- return unless document.body or activatedElement
- activatedElement ||= document.body
+ activatedElement ||= document.body and firstScrollableElement()
+ return unless activatedElement
element = findScrollableElement activatedElement, direction, pos, 1
amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
new file mode 100644
index 00000000..c4ed3bf6
--- /dev/null
+++ b/content_scripts/ui_component.coffee
@@ -0,0 +1,54 @@
+class UIComponent
+ iframeElement: null
+ iframePort: null
+ showing: null
+
+ 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
+ @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.
+ openPort: ->
+ messageChannel = new MessageChannel()
+ @iframePort = messageChannel.port1
+ @iframePort.onmessage = (event) => @handleMessage event
+
+ # 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
+
+ activate: (message) ->
+ @postMessage message if message?
+ 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.
+ @iframeElement.classList.add "vimiumUIComponentReactivated"
+ setTimeout((=> @iframeElement.classList.remove "vimiumUIComponentReactivated"), 200)
+ else
+ @show()
+ @iframeElement.focus()
+
+ show: (message) ->
+ @postMessage message if message?
+ @iframeElement.classList.remove "vimiumUIComponentHidden"
+ @iframeElement.classList.add "vimiumUIComponentShowing"
+ @showing = true
+
+ hide: (focusWindow = true)->
+ @iframeElement.classList.remove "vimiumUIComponentShowing"
+ @iframeElement.classList.add "vimiumUIComponentHidden"
+ window.focus() if focusWindow
+ @showing = false
+
+root = exports ? window
+root.UIComponent = UIComponent
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index cdf47d6c..ec1a09e6 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -271,140 +271,47 @@ 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: 2147483645;
}
-#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 {
+div#vimiumFlash {
+ box-shadow: 0px 0px 4px 2px #4183C4;
+ padding: 1px;
+ background-color: transparent;
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;
+ z-index: 2147483648;
}
-#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;
+/* UIComponent CSS */
+iframe.vimiumUIComponentHidden {
+ display: none;
}
-#vomnibar li.vomnibarSelected {
- background-color: #BBCEE9;
- font-weight: normal;
+iframe.vimiumUIComponentVisible {
+ display: block;
}
-
-
-div#vimiumFlash {
- box-shadow: 0px 0px 4px 2px #4183C4;
- padding: 1px;
- background-color: transparent;
- position: absolute;
- z-index: 2147483648;
+iframe.vimiumUIComponentReactivated {
+ border: 5px solid yellow;
}
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 1f116f88..351a2690 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -57,6 +57,15 @@ 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
+ for own property, value of this
+ # @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]
set: (key, value) ->
@@ -109,6 +118,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()
@@ -183,6 +199,8 @@ 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.
@@ -436,15 +454,16 @@ 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
+ # 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
+ KeydownEvents.push event
else if (findMode)
if (KeyboardUtils.isEscape(event))
@@ -1086,6 +1105,40 @@ Tween =
value = (elapsed / state.duration) * (state.to - state.from) + state.from
state.onUpdate(value)
+CursorHider =
+ #
+ # Hide the cursor when the browser scrolls, and prevent mouse from hovering while invisible.
+ #
+ cursorHideStyle: null
+ isScrolling: false
+
+ onScroll: (event) ->
+ CursorHider.isScrolling = true
+ 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: ->
+ # 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"
+
+ @cursorHideStyle = document.createElement("style")
+ @cursorHideStyle.innerHTML = """
+ body * {pointer-events: none !important; cursor: none !important;}
+ body, html {cursor: none !important;}
+ """
+ window.addEventListener "mousemove", @onMouseMove
+ window.addEventListener "scroll", @onScroll
+
initializePreDomReady()
window.addEventListener("DOMContentLoaded", registerFrame)
window.addEventListener("unload", unregisterFrame)
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index f1d2ccc5..6381fd7f 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -1,245 +1,51 @@
-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)
- activateEditUrl: -> @activateWithCompleter("omni", 100, window.location.href)
- activateEditUrlInNewTab: -> @activateWithCompleter("omni", 100, window.location.href, false, true)
- 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
-
- show: ->
- @box.style.display = "block"
- @input.focus()
- @handlerId = handlerStack.push keydown: @onKeydown.bind @
-
- 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()
- # <Enter> 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.
- DomUtils.suppressPropagation(event)
- 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) -> "<li>#{completion.html}</li>").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(
- """
- <div id="vomnibar" class="vimiumReset">
- <div class="vimiumReset vomnibarSearchArea">
- <input type="text" class="vimiumReset">
- </div>
- <ul class="vimiumReset"></ul>
- </div>
- """)
- @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.
+# This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar.
#
-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 })
+Vomnibar =
+ vomnibarUI: 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
+ }
+ activateEditUrl: -> @open {
+ completer: "omni"
+ selectFirst: false
+ query: window.location.href
+ }
+ activateEditUrlInNewTab: -> @open {
+ completer: "omni"
+ selectFirst: false
+ query: window.location.href
+ newTab: true
+ }
+
+ init: ->
+ unless @vomnibarUI?
+ @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.
+ # 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) -> @vomnibarUI.activate options
root = exports ? window
root.Vomnibar = Vomnibar