diff options
Diffstat (limited to 'content_scripts/vimium_frontend.coffee')
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 271 |
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() |
