aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/vimium_frontend.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts/vimium_frontend.coffee')
-rw-r--r--content_scripts/vimium_frontend.coffee271
1 files changed, 92 insertions, 179 deletions
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 6b96e929..4fdf58bd 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -5,18 +5,12 @@
# "domReady".
#
-targetElement = null
-findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
findModeQueryHasResults = false
findModeAnchorNode = null
findModeInitialRange = null
isShowingHelpDialog = false
keyPort = null
-# Users can disable Vimium on URL patterns via the settings page. The following two variables
-# (isEnabledForUrl and passKeys) control Vimium's enabled/disabled behaviour.
-# "passKeys" are keys which would normally be handled by Vimium, but are disabled on this tab, and therefore
-# are passed through to the underlying page.
isEnabledForUrl = true
passKeys = null
keyQueue = null
@@ -48,7 +42,7 @@ settings =
values: {}
loadedValues: 0
valuesToLoad: ["scrollStepSize", "linkHintCharacters", "linkHintNumbers", "filterLinkHints", "hideHud",
- "previousPatterns", "nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss",
+ "previousPatterns", "nextPatterns", "findModeRawQuery", "findModeRawQueryList", "regexFindMode", "userDefinedLinkHintCss",
"helpDialog_showAdvancedCommands", "smoothScroll"]
isLoaded: false
eventListeners: {}
@@ -101,15 +95,8 @@ settings =
#
frameId = Math.floor(Math.random()*999999999)
-hasModifiersRegex = /^<([amc]-)+.>/
-
-#
-# Complete initialization work that sould be done prior to DOMReady.
-#
-initializePreDomReady = ->
- settings.addEventListener("load", LinkHints.init.bind(LinkHints))
- settings.load()
-
+# Only exported for tests.
+window.initializeModes = ->
class NormalMode extends Mode
constructor: ->
super
@@ -122,12 +109,20 @@ initializePreDomReady = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
+ new BadgeMode
new NormalMode
new PassKeysMode
new InsertMode permanent: true
- checkIfEnabledForUrl()
+#
+# Complete initialization work that sould be done prior to DOMReady.
+#
+initializePreDomReady = ->
+ settings.addEventListener("load", LinkHints.init.bind(LinkHints))
+ settings.load()
+ initializeModes()
+ checkIfEnabledForUrl()
refreshCompletionKeys()
# Send the key to the key handler in the background page.
@@ -179,25 +174,22 @@ installListener = (element, event, callback) ->
# Run this as early as possible, so the page can't register any event handlers before us.
#
installedListeners = false
-initializeWhenEnabled = (newPassKeys) ->
- isEnabledForUrl = true
- passKeys = newPassKeys
- if (!installedListeners)
+window.initializeWhenEnabled = ->
+ unless installedListeners
# Key event handlers fire on window before they do on document. Prefer window for key events so the page
# can't set handlers to grab the keys before us.
for type in ["keydown", "keypress", "keyup", "click", "focus", "blur"]
do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
- installListener document, "DOMActivate", onDOMActivate
- enterInsertModeIfElementIsFocused()
+ installListener document, "DOMActivate", (event) -> handlerStack.bubbleEvent 'DOMActivate', event
installedListeners = true
setState = (request) ->
- initializeWhenEnabled(request.passKeys) if request.enabled
isEnabledForUrl = request.enabled
passKeys = request.passKeys
+ initializeWhenEnabled() if isEnabledForUrl
handlerStack.bubbleEvent "registerStateChange",
- enabled: request.enabled
- passKeys: request.passKeys
+ enabled: isEnabledForUrl
+ passKeys: passKeys
getActiveState = ->
Mode.updateBadge()
@@ -215,8 +207,6 @@ window.addEventListener "focus", ->
# Initialization tasks that must wait for the document to be ready.
#
initializeOnDomReady = ->
- enterInsertModeIfElementIsFocused() if isEnabledForUrl
-
# Tell the background page we're in the dom ready state.
chrome.runtime.connect({ name: "domReady" })
CursorHider.init()
@@ -236,15 +226,6 @@ unregisterFrame = ->
frameId: frameId
tab_is_closing: window.top == window.self
-#
-# Enters insert mode if the currently focused element in the DOM is focusable.
-#
-enterInsertModeIfElementIsFocused = ->
- if (document.activeElement && isEditable(document.activeElement) && !findMode)
- enterInsertModeWithoutShowingIndicator(document.activeElement)
-
-onDOMActivate = (event) -> handlerStack.bubbleEvent 'DOMActivate', event
-
executePageCommand = (request) ->
return unless frameId == request.frameId
@@ -340,11 +321,9 @@ extend window,
focusInput: do ->
# Track the most recently focused input element.
recentlyFocusedElement = null
- handlerStack.push
- _name: "focus-input-tracker"
- focus: (event) ->
- recentlyFocusedElement = event.target if DomUtils.isEditable event.target
- true
+ window.addEventListener "focus",
+ (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target
+ , true
(count, mode = InsertMode) ->
# Focus the first input element on the page, and create overlays to highlight all the input elements, with
@@ -359,7 +338,9 @@ extend window,
continue if rect == null
{ element: element, rect: rect }
- return if visibleInputs.length == 0
+ if visibleInputs.length == 0
+ HUD.showForDuration("There are no inputs to focus.", 1000)
+ return
selectedInputIndex =
if count == 1
@@ -423,13 +404,6 @@ extend window,
singleton: document.activeElement
targetElement: document.activeElement
-# Decide whether this keyChar should be passed to the underlying page.
-# Keystrokes are *never* considered passKeys if the keyQueue is not empty. So, for example, if 't' is a
-# passKey, then 'gt' and '99t' will neverthless be handled by vimium.
-isPassKey = ( keyChar ) ->
- return false # Disabled.
- return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar)
-
# Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup
# event.
KeydownEvents =
@@ -469,25 +443,13 @@ onKeypress = (event) ->
if (event.keyCode > 31)
keyChar = String.fromCharCode(event.charCode)
- # Enter insert mode when the user enables the native find interface.
- if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event))
- enterInsertModeWithoutShowingIndicator()
- return @stopBubblingAndTrue
-
if (keyChar)
- if (findMode)
- handleKeyCharForFindMode(keyChar)
+ if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
DomUtils.suppressEvent(event)
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @stopBubblingAndTrue
- else if (!isInsertMode() && !findMode)
- if (isPassKey keyChar)
- return @stopBubblingAndTrue
- if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
- DomUtils.suppressEvent(event)
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
- return @stopBubblingAndTrue
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @continueBubbling
@@ -520,50 +482,13 @@ onKeydown = (event) ->
if (modifiers.length > 0 || keyChar.length > 1)
keyChar = "<" + keyChar + ">"
- if (isInsertMode() && KeyboardUtils.isEscape(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
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (findMode)
- if (KeyboardUtils.isEscape(event))
- handleEscapeForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
- handleDeleteForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (event.keyCode == keyCodes.enter)
- handleEnterForFindMode()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (!modifiers)
- DomUtils.suppressPropagation(event)
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
+ if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
DomUtils.suppressEvent event
KeydownEvents.push event
return @stopBubblingAndTrue
- else if (!isInsertMode() && !findMode)
+ else
if (keyChar)
if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent event
@@ -576,9 +501,6 @@ onKeydown = (event) ->
else if (KeyboardUtils.isEscape(event))
keyPort.postMessage({ keyChar:"<ESC>", frameId:frameId })
- else if isPassKey KeyboardUtils.getKeyChar(event)
- return undefined
-
# Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command.
# The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us
# back into the search box. As a side effect, this should also prevent overriding by other sites.
@@ -586,9 +508,9 @@ onKeydown = (event) ->
# Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress).
#
# TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
- if (keyChar == "" && !isInsertMode() &&
+ if keyChar == "" &&
(currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
- isValidFirstKey(KeyboardUtils.getKeyChar(event))))
+ isValidFirstKey(KeyboardUtils.getKeyChar(event)))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
return @stopBubblingAndTrue
@@ -606,14 +528,15 @@ checkIfEnabledForUrl = ->
chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url }, (response) ->
isEnabledForUrl = response.isEnabledForUrl
- if (isEnabledForUrl)
- initializeWhenEnabled(response.passKeys)
+ passKeys = response.passKeys
+ if isEnabledForUrl
+ initializeWhenEnabled()
else if (HUD.isReady())
# Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
HUD.hide()
handlerStack.bubbleEvent "registerStateChange",
- enabled: response.isEnabledForUrl
- passKeys: response.passKeys
+ enabled: isEnabledForUrl
+ passKeys: passKeys
# Exported to window, but only for DOM tests.
window.refreshCompletionKeys = (response) ->
@@ -628,57 +551,27 @@ window.refreshCompletionKeys = (response) ->
isValidFirstKey = (keyChar) ->
validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
-onFocusCapturePhase = (event) ->
- if (isFocusable(event.target) && !findMode)
- enterInsertModeWithoutShowingIndicator(event.target)
-
-onBlurCapturePhase = (event) ->
- if (isFocusable(event.target))
- exitInsertMode(event.target)
-
-#
-# Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus.
-#
-isFocusable = (element) -> isEditable(element) || isEmbed(element)
-
-#
-# Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
-# unfocused.
-#
-isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase()) >= 0
-
-#
-# Input or text elements are considered focusable and able to receieve their own keyboard events,
-# and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on
-# any element which makes it a rich text editor, like the notes on jjot.com.
-#
-isEditable = (target) ->
- # Note: document.activeElement.isContentEditable is also rechecked in isInsertMode() dynamically.
- return true if target.isContentEditable
- nodeName = target.nodeName.toLowerCase()
- # use a blacklist instead of a whitelist because new form controls are still being implemented for html5
- noFocus = ["radio", "checkbox"]
- if (nodeName == "input" && noFocus.indexOf(target.type) == -1)
- return true
- focusableElements = ["textarea", "select"]
- focusableElements.indexOf(nodeName) >= 0
-
-#
-# We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A
-# causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode
-# when the last editable element that came into focus -- which targetElement points to -- has been blurred.
-# If insert mode is entered manually (via pressing 'i'), then we set targetElement to 'undefined', and only
-# leave insert mode when the user presses <ESC>.
-# Note. This returns the truthiness of target, which is required by isInsertMode.
-#
-enterInsertModeWithoutShowingIndicator = (target) ->
- return # Disabled.
-
-exitInsertMode = (target) ->
- return # Disabled.
-
-isInsertMode = ->
- return false # Disabled.
+# This implements a find-mode query history (using the "findModeRawQueryList" setting) as a list of raw
+# queries, most recent first.
+FindModeHistory =
+ getQuery: (index = 0) ->
+ @migration()
+ recentQueries = settings.get "findModeRawQueryList"
+ if index < recentQueries.length then recentQueries[index] else ""
+
+ recordQuery: (query) ->
+ @migration()
+ if 0 < query.length
+ recentQueries = settings.get "findModeRawQueryList"
+ settings.set "findModeRawQueryList", ([ query ].concat recentQueries.filter (q) -> q != query)[0..50]
+
+ # Migration (from 1.49, 2015/2/1).
+ # Legacy setting: findModeRawQuery (a string).
+ # New setting: findModeRawQueryList (a list of strings).
+ migration: ->
+ unless settings.get "findModeRawQueryList"
+ rawQuery = settings.get "findModeRawQuery"
+ settings.set "findModeRawQueryList", (if rawQuery then [ rawQuery ] else [])
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
@@ -706,6 +599,9 @@ updateFindModeQuery = ->
# default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(findModeQuery.parsedQuery)
+ # Don't count matches in the HUD.
+ HUD.hide(true)
+
# if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
# sequentially so the browser handles the scrolling / text selection.
if findModeQuery.isRegex
@@ -731,12 +627,15 @@ updateFindModeQuery = ->
text = document.body.innerText
findModeQuery.matchCount = text.match(pattern)?.length
-handleKeyCharForFindMode = (keyChar) ->
- findModeQuery.rawQuery += keyChar
+updateQueryForFindMode = (rawQuery) ->
+ findModeQuery.rawQuery = rawQuery
updateFindModeQuery()
performFindInPlace()
showFindModeHUDForQuery()
+handleKeyCharForFindMode = (keyChar) ->
+ updateQueryForFindMode findModeQuery.rawQuery + keyChar
+
handleEscapeForFindMode = ->
exitFindMode()
document.body.classList.remove("vimiumFindMode")
@@ -749,15 +648,15 @@ handleEscapeForFindMode = ->
window.getSelection().addRange(range)
focusFoundLink() || selectFoundInputElement()
+# Return true if character deleted, false otherwise.
handleDeleteForFindMode = ->
- if (findModeQuery.rawQuery.length == 0)
+ if findModeQuery.rawQuery.length == 0
exitFindMode()
performFindInPlace()
+ false
else
- findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
- updateFindModeQuery()
- performFindInPlace()
- showFindModeHUDForQuery()
+ updateQueryForFindMode findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
+ true
# <esc> sends us into insert mode if possible, but <cr> does not.
# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
@@ -766,10 +665,12 @@ handleEnterForFindMode = ->
exitFindMode()
focusFoundLink()
document.body.classList.add("vimiumFindMode")
- settings.set("findModeRawQuery", findModeQuery.rawQuery)
+ FindModeHistory.recordQuery findModeQuery.rawQuery
class FindMode extends Mode
constructor: ->
+ @historyIndex = -1
+ @partialQuery = ""
super
name: "find"
badge: "/"
@@ -778,12 +679,23 @@ class FindMode extends Mode
keydown: (event) =>
if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
- handleDeleteForFindMode()
+ @exit() unless handleDeleteForFindMode()
@suppressEvent
else if event.keyCode == keyCodes.enter
handleEnterForFindMode()
@exit()
@suppressEvent
+ else if event.keyCode == keyCodes.upArrow
+ if rawQuery = FindModeHistory.getQuery @historyIndex + 1
+ @historyIndex += 1
+ @partialQuery = findModeQuery.rawQuery if @historyIndex == 0
+ updateQueryForFindMode rawQuery
+ @suppressEvent
+ else if event.keyCode == keyCodes.downArrow
+ @historyIndex = Math.max -1, @historyIndex - 1
+ rawQuery = if 0 <= @historyIndex then FindModeHistory.getQuery @historyIndex else @partialQuery
+ updateQueryForFindMode rawQuery
+ @suppressEvent
else
DomUtils.suppressPropagation(event)
handlerStack.stopBubblingAndFalse
@@ -854,8 +766,6 @@ selectFoundInputElement = ->
DomUtils.isSelectable(document.activeElement) &&
DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
DomUtils.simulateSelect(document.activeElement)
- # the element has already received focus via find(), so invoke insert mode manually
- enterInsertModeWithoutShowingIndicator(document.activeElement)
getNextQueryFromRegexMatches = (stepSize) ->
# find()ing an empty query always returns false
@@ -869,7 +779,7 @@ getNextQueryFromRegexMatches = (stepSize) ->
window.getFindModeQuery = ->
# check if the query has been changed by a script in another frame
- mostRecentQuery = settings.get("findModeRawQuery") || ""
+ mostRecentQuery = FindModeHistory.getQuery()
if (mostRecentQuery != findModeQuery.rawQuery)
findModeQuery.rawQuery = mostRecentQuery
updateFindModeQuery()
@@ -997,10 +907,13 @@ window.goNext = ->
findAndFollowRel("next") || findAndFollowLink(nextStrings)
showFindModeHUDForQuery = ->
- if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
- HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Matches)")
- else
+ if findModeQuery.rawQuery and (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
+ plural = if findModeQuery.matchCount == 1 then "" else "es"
+ HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Match#{plural})")
+ else if findModeQuery.rawQuery
HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")
+ else
+ HUD.show("/")
getCurrentRange = ->
selection = getSelection()