aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/hud.coffee6
-rw-r--r--content_scripts/mode_find.coffee52
-rw-r--r--content_scripts/mode_normal.coffee346
-rw-r--r--content_scripts/vimium_frontend.coffee395
-rw-r--r--lib/utils.coffee8
-rw-r--r--manifest.json1
-rw-r--r--pages/blank.html1
-rw-r--r--pages/completion_engines.html1
-rw-r--r--pages/help_dialog.html1
-rw-r--r--pages/logging.html1
-rw-r--r--pages/options.html1
-rw-r--r--tests/dom_tests/dom_tests.coffee26
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/unit_tests/commands_test.coffee22
-rw-r--r--tests/unit_tests/utils_test.coffee20
15 files changed, 453 insertions, 429 deletions
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index 6185d786..7c983cfa 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -71,13 +71,13 @@ HUD =
focusNode?.focus?()
if exitEventIsEnter
- handleEnterForFindMode()
+ FindMode.handleEnter()
if FindMode.query.hasResults
postExit = -> new PostFindMode
else if exitEventIsEscape
- # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we
+ # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we
# wait until the mode is closed before running it.
- postExit = handleEscapeForFindMode
+ postExit = FindMode.handleEscape
@findMode.exit()
postExit?()
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index 5238ab34..b6c80cec 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -79,7 +79,7 @@ class FindMode extends Mode
exit: (event) ->
super()
- handleEscapeForFindMode() if event
+ FindMode.handleEscape() if event
restoreSelection: ->
range = @initialRange
@@ -201,6 +201,34 @@ class FindMode extends Mode
@restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode")
+ # The user has found what they're looking for and is finished searching. We enter insert mode, if possible.
+ @handleEscape: ->
+ document.body.classList.remove("vimiumFindMode")
+ # Removing the class does not re-color existing selections. we recreate the current selection so it reverts
+ # back to the default color.
+ selection = window.getSelection()
+ unless selection.isCollapsed
+ range = window.getSelection().getRangeAt(0)
+ window.getSelection().removeAllRanges()
+ window.getSelection().addRange(range)
+ focusFoundLink() || selectFoundInputElement()
+
+ # Save the query so the user can do further searches with it.
+ @handleEnter: ->
+ focusFoundLink()
+ document.body.classList.add("vimiumFindMode")
+ FindMode.saveQuery()
+
+ @findNext: (backwards) ->
+ Marks.setPreviousPosition()
+ FindMode.query.hasResults = FindMode.execute null, {backwards}
+
+ if FindMode.query.hasResults
+ focusFoundLink()
+ new PostFindMode()
+ else
+ HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
+
checkReturnToViewPort: ->
window.scrollTo @scrollX, @scrollY if @options.returnToViewport
@@ -215,6 +243,28 @@ getCurrentRange = ->
selection.collapseToStart() if selection.type == "Range"
selection.getRangeAt 0
+getLinkFromSelection = ->
+ node = window.getSelection().anchorNode
+ while (node && node != document.body)
+ return node if (node.nodeName.toLowerCase() == "a")
+ node = node.parentNode
+ null
+
+focusFoundLink = ->
+ if (FindMode.query.hasResults)
+ link = getLinkFromSelection()
+ link.focus() if link
+
+selectFoundInputElement = ->
+ # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
+ # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
+ # that the last anchor node is an ancestor of our element.
+ findModeAnchorNode = document.getSelection().anchorNode
+ if (FindMode.query.hasResults && document.activeElement &&
+ DomUtils.isSelectable(document.activeElement) &&
+ DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
+ DomUtils.simulateSelect(document.activeElement)
+
root = exports ? (window.root ?= {})
root.PostFindMode = PostFindMode
root.FindMode = FindMode
diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee
new file mode 100644
index 00000000..ee05f4b0
--- /dev/null
+++ b/content_scripts/mode_normal.coffee
@@ -0,0 +1,346 @@
+class NormalMode extends KeyHandlerMode
+ constructor: (options = {}) ->
+ defaults =
+ name: "normal"
+ indicator: false # There is normally no mode indicator in normal mode.
+ commandHandler: @commandHandler.bind this
+
+ super extend defaults, options
+
+ chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
+ @setKeyMapping items.normalModeKeyStateMapping
+
+ chrome.storage.onChanged.addListener (changes, area) =>
+ if area == "local" and changes.normalModeKeyStateMapping?.newValue
+ @setKeyMapping changes.normalModeKeyStateMapping.newValue
+
+ commandHandler: ({command: registryEntry, count}) ->
+ count *= registryEntry.options.count ? 1
+ count = 1 if registryEntry.noRepeat
+
+ if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
+ return unless confirm """
+ You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
+ Are you sure you want to continue?"""
+
+ if registryEntry.topFrame
+ # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
+ sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
+ chrome.runtime.sendMessage
+ handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
+ else if registryEntry.background
+ chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
+ else
+ NormalModeCommands[registryEntry.command] count, {registryEntry}
+
+enterNormalMode = (count) ->
+ new NormalMode
+ indicator: "Normal mode (pass keys disabled)"
+ exitOnEscape: true
+ singleton: "enterNormalMode"
+ count: count
+
+NormalModeCommands =
+ # Scrolling.
+ scrollToBottom: ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", "max"
+ scrollToTop: (count) ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
+ scrollToLeft: -> Scroller.scrollTo "x", 0
+ scrollToRight: -> Scroller.scrollTo "x", "max"
+ scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
+ scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
+ scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
+ scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
+ scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
+ scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
+ scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
+ scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
+
+ # Page state.
+ reload: (count, options) ->
+ hard = options.registryEntry.options.hard ? false
+ window.location.reload(hard)
+ goBack: (count) -> history.go(-count)
+ goForward: (count) -> history.go(count)
+
+ # Url manipulation.
+ goUp: (count) ->
+ url = window.location.href
+ if (url[url.length - 1] == "/")
+ url = url.substring(0, url.length - 1)
+
+ urlsplit = url.split("/")
+ # make sure we haven't hit the base domain yet
+ if (urlsplit.length > 3)
+ urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
+ window.location.href = urlsplit.join('/')
+
+ goToRoot: ->
+ window.location.href = window.location.origin
+
+ toggleViewSource: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ if (url.substr(0, 12) == "view-source:")
+ url = url.substr(12, url.length - 12)
+ else
+ url = "view-source:" + url
+ chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
+
+ copyCurrentUrl: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ chrome.runtime.sendMessage { handler: "copyToClipboard", data: url }
+ url = url[0..25] + "...." if 28 < url.length
+ HUD.showForDuration("Yanked #{url}", 2000)
+
+ # Mode changes.
+ enterInsertMode: ->
+ # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
+ # instance to take over.
+ new InsertMode global: true, exitOnFocus: true
+
+ enterVisualMode: ->
+ new VisualMode userLaunchedMode: true
+
+ enterVisualLineMode: ->
+ new VisualLineMode userLaunchedMode: true
+
+ enterFindMode: ->
+ Marks.setPreviousPosition()
+ new FindMode()
+
+ # Find.
+ performFind: (count) -> FindMode.findNext false for [0...count] by 1
+ performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1
+
+ # Misc.
+ mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
+ showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
+
+ passNextKey: (count, options) ->
+ if options.registryEntry.options.normal
+ enterNormalMode count
+ else
+ new PassNextKeyMode count
+
+ goPrevious: ->
+ previousPatterns = Settings.get("previousPatterns") || ""
+ previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("prev") || findAndFollowLink(previousStrings)
+
+ goNext: ->
+ nextPatterns = Settings.get("nextPatterns") || ""
+ nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("next") || findAndFollowLink(nextStrings)
+
+ focusInput: (count) ->
+ # Focus the first input element on the page, and create overlays to highlight all the input elements, with
+ # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
+ # Pressing any other key will remove the overlays and the special tab behavior.
+ resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
+ visibleInputs =
+ for i in [0...resultSet.snapshotLength] by 1
+ element = resultSet.snapshotItem i
+ continue unless DomUtils.getVisibleClientRect element, true
+ { element, rect: Rect.copy element.getBoundingClientRect() }
+
+ if visibleInputs.length == 0
+ HUD.showForDuration("There are no inputs to focus.", 1000)
+ return
+
+ # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
+ # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
+ recentlyFocusedElement = lastFocusedInput()
+ recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage
+
+ selectedInputIndex =
+ if count == 1
+ # As the starting index, we pick that of the most recently focused input element (or 0).
+ elements = visibleInputs.map (visibleInput) -> visibleInput.element
+ Math.max 0, elements.indexOf recentlyFocusedElement
+ else
+ Math.min(count, visibleInputs.length) - 1
+
+ hints = for tuple in visibleInputs
+ hint = DomUtils.createElement "div"
+ hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
+
+ # minus 1 for the border
+ hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
+ hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
+ hint.style.width = tuple.rect.width + "px"
+ hint.style.height = tuple.rect.height + "px"
+
+ hint
+
+ new FocusSelector hints, visibleInputs, selectedInputIndex
+
+if LinkHints?
+ extend NormalModeCommands,
+ "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints
+ "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints
+ "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints
+ "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints
+ "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints
+ "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints
+ "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints
+
+if Vomnibar?
+ extend NormalModeCommands,
+ "Vomnibar.activate": Vomnibar.activate.bind Vomnibar
+ "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar
+ "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar
+ "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar
+ "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar
+ "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar
+ "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar
+
+if Marks?
+ extend NormalModeCommands,
+ "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks
+ "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks
+
+# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
+# each content script. Alternatively we could calculate it once in the background page and use a request to
+# fetch it each time.
+# Should we include the HTML5 date pickers here?
+
+# The corresponding XPath for such elements.
+textInputXPath = (->
+ textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
+ inputElements = ["input[" +
+ "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
+ " and not(@disabled or @readonly)]",
+ "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
+ DomUtils?.makeXPath(inputElements)
+)()
+
+# used by the findAndFollow* functions.
+followLink = (linkElement) ->
+ if (linkElement.nodeName.toLowerCase() == "link")
+ window.location.href = linkElement.href
+ else
+ # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
+ # calls, like the 'more' button on GitHub's newsfeed.
+ linkElement.scrollIntoView()
+ DomUtils.simulateClick(linkElement)
+
+#
+# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
+# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
+# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
+# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
+#
+findAndFollowLink = (linkStrings) ->
+ linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
+ links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
+ candidateLinks = []
+
+ # at the end of this loop, candidateLinks will contain all visible links that match our patterns
+ # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
+ for i in [(links.snapshotLength - 1)..0] by -1
+ link = links.snapshotItem(i)
+
+ # ensure link is visible (we don't mind if it is scrolled offscreen)
+ boundingClientRect = link.getBoundingClientRect()
+ if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
+ continue
+ computedStyle = window.getComputedStyle(link, null)
+ if (computedStyle.getPropertyValue("visibility") != "visible" ||
+ computedStyle.getPropertyValue("display") == "none")
+ continue
+
+ linkMatches = false
+ for linkString in linkStrings
+ if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
+ 0 <= link.value?.indexOf? linkString
+ linkMatches = true
+ break
+ continue unless linkMatches
+
+ candidateLinks.push(link)
+
+ return if (candidateLinks.length == 0)
+
+ for link in candidateLinks
+ link.wordCount = link.innerText.trim().split(/\s+/).length
+
+ # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
+ # in-page order of the links.
+
+ candidateLinks.forEach((a,i) -> a.originalIndex = i)
+
+ # favor shorter links, and ignore those that are more than one word longer than the shortest link
+ candidateLinks =
+ candidateLinks
+ .sort((a, b) ->
+ if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
+ )
+ .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
+
+ for linkString in linkStrings
+ exactWordRegex =
+ if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
+ new RegExp "\\b" + linkString + "\\b", "i"
+ else
+ new RegExp linkString, "i"
+ for candidateLink in candidateLinks
+ if exactWordRegex.test(candidateLink.innerText) ||
+ (candidateLink.value && exactWordRegex.test(candidateLink.value))
+ followLink(candidateLink)
+ return true
+ false
+
+findAndFollowRel = (value) ->
+ relTags = ["link", "a", "area"]
+ for tag in relTags
+ elements = document.getElementsByTagName(tag)
+ for element in elements
+ if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
+ followLink(element)
+ return true
+
+class FocusSelector extends Mode
+ constructor: (hints, visibleInputs, selectedInputIndex) ->
+ super
+ name: "focus-selector"
+ exitOnClick: true
+ keydown: (event) =>
+ if event.key == "Tab"
+ hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
+ selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
+ selectedInputIndex %= hints.length
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ @suppressEvent
+ else unless event.key == "Shift"
+ @exit()
+ # Give the new mode the opportunity to handle the event.
+ @restartBubbling
+
+ @hintContainingDiv = DomUtils.addElementList hints,
+ id: "vimiumInputMarkerContainer"
+ className: "vimiumReset"
+
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ if visibleInputs.length == 1
+ @exit()
+ return
+ else
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+
+ exit: ->
+ super()
+ DomUtils.removeElement @hintContainingDiv
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ new InsertMode
+ singleton: "post-find-mode/focus-input"
+ targetElement: document.activeElement
+ indicator: false
+
+root = exports ? (window.root ?= {})
+root.NormalMode = NormalMode
+root.NormalModeCommands = NormalModeCommands
+extend window, root unless exports?
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 86e3b682..bbc3e4f1 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -22,21 +22,6 @@ windowIsFocused = do ->
windowHasFocus = false if event.target == window; true
-> windowHasFocus
-# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
-# each content script. Alternatively we could calculate it once in the background page and use a request to
-# fetch it each time.
-# Should we include the HTML5 date pickers here?
-
-# The corresponding XPath for such elements.
-textInputXPath = (->
- textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
- inputElements = ["input[" +
- "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
- " and not(@disabled or @readonly)]",
- "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
- DomUtils.makeXPath(inputElements)
-)()
-
# This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab.
frameId = null
@@ -117,41 +102,6 @@ handlerStack.push
target = target.parentElement
true
-class NormalMode extends KeyHandlerMode
- constructor: (options = {}) ->
- defaults =
- name: "normal"
- indicator: false # There is normally no mode indicator in normal mode.
- commandHandler: @commandHandler.bind this
-
- super extend defaults, options
-
- chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
- @setKeyMapping items.normalModeKeyStateMapping
-
- chrome.storage.onChanged.addListener (changes, area) =>
- if area == "local" and changes.normalModeKeyStateMapping?.newValue
- @setKeyMapping changes.normalModeKeyStateMapping.newValue
-
- commandHandler: ({command: registryEntry, count}) ->
- count *= registryEntry.options.count ? 1
- count = 1 if registryEntry.noRepeat
-
- if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
- return unless confirm """
- You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
- Are you sure you want to continue?"""
-
- if registryEntry.topFrame
- # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
- sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
- chrome.runtime.sendMessage
- handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
- else if registryEntry.background
- chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
- else
- Utils.invokeCommandString registryEntry.command, count, {registryEntry}
-
installModes = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
@@ -192,7 +142,7 @@ initializePreDomReady = ->
frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this).
checkEnabledAfterURLChange: checkEnabledAfterURLChange
runInTopFrame: ({sourceFrameId, registryEntry}) ->
- Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame()
+ NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame()
linkHintsMessage: (request) -> HintCoordinator[request.messageType] request
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
@@ -333,176 +283,17 @@ focusThisFrame = (request) ->
document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe"
flashFrame() if request.highlight
-extend root,
- scrollToBottom: ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", "max"
- scrollToTop: (count) ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
- scrollToLeft: -> Scroller.scrollTo "x", 0
- scrollToRight: -> Scroller.scrollTo "x", "max"
- scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
- scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
- scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
- scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
- scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
- scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
- scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
- scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
-
-extend root,
- reload: (count, options) ->
- hard = options.registryEntry.options.hard ? false
- window.location.reload(hard)
- goBack: (count) -> history.go(-count)
- goForward: (count) -> history.go(count)
-
- goUp: (count) ->
- url = window.location.href
- if (url[url.length - 1] == "/")
- url = url.substring(0, url.length - 1)
-
- urlsplit = url.split("/")
- # make sure we haven't hit the base domain yet
- if (urlsplit.length > 3)
- urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
- window.location.href = urlsplit.join('/')
-
- goToRoot: ->
- window.location.href = window.location.origin
-
- mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
-
- toggleViewSource: ->
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- if (url.substr(0, 12) == "view-source:")
- url = url.substr(12, url.length - 12)
- else
- url = "view-source:" + url
- chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
-
- copyCurrentUrl: ->
- # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background
- # page to copy.
- # http://code.google.com/p/chromium/issues/detail?id=55188
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- chrome.runtime.sendMessage { handler: "copyToClipboard", data: url }
- url = url[0..25] + "...." if 28 < url.length
- HUD.showForDuration("Yanked #{url}", 2000)
-
- enterInsertMode: ->
- # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
- # instance to take over.
- new InsertMode global: true, exitOnFocus: true
-
- enterVisualMode: ->
- new VisualMode userLaunchedMode: true
-
- enterVisualLineMode: ->
- new VisualLineMode userLaunchedMode: true
-
- passNextKey: (count, options) ->
- if options.registryEntry.options.normal
- enterNormalMode count
- else
- new PassNextKeyMode count
-
- enterNormalMode: (count) ->
- new NormalMode
- indicator: "Normal mode (pass keys disabled)"
- exitOnEscape: true
- singleton: "enterNormalMode"
- count: count
-
- focusInput: do ->
- # Track the most recently focused input element.
- recentlyFocusedElement = null
- window.addEventListener "focus",
- forTrusted (event) ->
- DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996.
- if DomUtils.isEditable event.target
- recentlyFocusedElement = event.target
- , true
-
- (count) ->
- mode = InsertMode
- # Focus the first input element on the page, and create overlays to highlight all the input elements, with
- # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
- # Pressing any other key will remove the overlays and the special tab behavior.
- # The mode argument is the mode to enter once an input is selected.
- resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
- visibleInputs =
- for i in [0...resultSet.snapshotLength] by 1
- element = resultSet.snapshotItem i
- continue unless DomUtils.getVisibleClientRect element, true
- { element, rect: Rect.copy element.getBoundingClientRect() }
-
- if visibleInputs.length == 0
- HUD.showForDuration("There are no inputs to focus.", 1000)
- return
-
- # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
- # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
- recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage
-
- selectedInputIndex =
- if count == 1
- # As the starting index, we pick that of the most recently focused input element (or 0).
- elements = visibleInputs.map (visibleInput) -> visibleInput.element
- Math.max 0, elements.indexOf recentlyFocusedElement
- else
- Math.min(count, visibleInputs.length) - 1
-
- hints = for tuple in visibleInputs
- hint = DomUtils.createElement "div"
- hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
-
- # minus 1 for the border
- hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
- hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
- hint.style.width = tuple.rect.width + "px"
- hint.style.height = tuple.rect.height + "px"
-
- hint
-
- new class FocusSelector extends Mode
- constructor: ->
- super
- name: "focus-selector"
- exitOnClick: true
- keydown: (event) =>
- if event.key == "Tab"
- hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
- selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
- selectedInputIndex %= hints.length
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- @suppressEvent
- else unless event.key == "Shift"
- @exit()
- # Give the new mode the opportunity to handle the event.
- @restartBubbling
-
- @hintContainingDiv = DomUtils.addElementList hints,
- id: "vimiumInputMarkerContainer"
- className: "vimiumReset"
-
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- if visibleInputs.length == 1
- @exit()
- return
- else
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
-
- exit: ->
- super()
- DomUtils.removeElement @hintContainingDiv
- if mode and document.activeElement and DomUtils.isEditable document.activeElement
- new mode
- singleton: "post-find-mode/focus-input"
- targetElement: document.activeElement
- indicator: false
+# Used by focusInput command.
+root.lastFocusedInput = do ->
+ # Track the most recently focused input element.
+ recentlyFocusedElement = null
+ window.addEventListener "focus",
+ forTrusted (event) ->
+ DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996.
+ if DomUtils.isEditable event.target
+ recentlyFocusedElement = event.target
+ , true
+ -> recentlyFocusedElement
# Checks if Vimium should be enabled or not in this frame. As a side effect, it also informs the background
# page whether this frame has the focus, allowing the background page to track the active frame's URL and set
@@ -524,163 +315,6 @@ checkIfEnabledForUrl = do ->
checkEnabledAfterURLChange = forTrusted ->
checkIfEnabledForUrl() if windowIsFocused()
-handleEscapeForFindMode = ->
- document.body.classList.remove("vimiumFindMode")
- # removing the class does not re-color existing selections. we recreate the current selection so it reverts
- # back to the default color.
- selection = window.getSelection()
- unless selection.isCollapsed
- range = window.getSelection().getRangeAt(0)
- window.getSelection().removeAllRanges()
- window.getSelection().addRange(range)
- focusFoundLink() || selectFoundInputElement()
-
-# <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
-# this query and do more searches with it'
-handleEnterForFindMode = ->
- focusFoundLink()
- document.body.classList.add("vimiumFindMode")
- FindMode.saveQuery()
-
-focusFoundLink = ->
- if (FindMode.query.hasResults)
- link = getLinkFromSelection()
- link.focus() if link
-
-selectFoundInputElement = ->
- # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
- # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
- # that the last anchor node is an ancestor of our element.
- findModeAnchorNode = document.getSelection().anchorNode
- if (FindMode.query.hasResults && document.activeElement &&
- DomUtils.isSelectable(document.activeElement) &&
- DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
- DomUtils.simulateSelect(document.activeElement)
-
-findAndFocus = (backwards) ->
- Marks.setPreviousPosition()
- FindMode.query.hasResults = FindMode.execute null, {backwards}
-
- if FindMode.query.hasResults
- focusFoundLink()
- new PostFindMode()
- else
- HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
-
-performFind = (count) -> findAndFocus false for [0...count] by 1
-performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1
-
-getLinkFromSelection = ->
- node = window.getSelection().anchorNode
- while (node && node != document.body)
- return node if (node.nodeName.toLowerCase() == "a")
- node = node.parentNode
- null
-
-# used by the findAndFollow* functions.
-followLink = (linkElement) ->
- if (linkElement.nodeName.toLowerCase() == "link")
- window.location.href = linkElement.href
- else
- # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
- # calls, like the 'more' button on GitHub's newsfeed.
- linkElement.scrollIntoView()
- DomUtils.simulateClick(linkElement)
-
-#
-# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
-# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
-# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
-# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
-#
-findAndFollowLink = (linkStrings) ->
- linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
- links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
- candidateLinks = []
-
- # at the end of this loop, candidateLinks will contain all visible links that match our patterns
- # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
- for i in [(links.snapshotLength - 1)..0] by -1
- link = links.snapshotItem(i)
-
- # ensure link is visible (we don't mind if it is scrolled offscreen)
- boundingClientRect = link.getBoundingClientRect()
- if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
- continue
- computedStyle = window.getComputedStyle(link, null)
- if (computedStyle.getPropertyValue("visibility") != "visible" ||
- computedStyle.getPropertyValue("display") == "none")
- continue
-
- linkMatches = false
- for linkString in linkStrings
- if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
- 0 <= link.value?.indexOf? linkString
- linkMatches = true
- break
- continue unless linkMatches
-
- candidateLinks.push(link)
-
- return if (candidateLinks.length == 0)
-
- for link in candidateLinks
- link.wordCount = link.innerText.trim().split(/\s+/).length
-
- # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
- # in-page order of the links.
-
- candidateLinks.forEach((a,i) -> a.originalIndex = i)
-
- # favor shorter links, and ignore those that are more than one word longer than the shortest link
- candidateLinks =
- candidateLinks
- .sort((a, b) ->
- if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
- )
- .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
-
- for linkString in linkStrings
- exactWordRegex =
- if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
- new RegExp "\\b" + linkString + "\\b", "i"
- else
- new RegExp linkString, "i"
- for candidateLink in candidateLinks
- if exactWordRegex.test(candidateLink.innerText) ||
- (candidateLink.value && exactWordRegex.test(candidateLink.value))
- followLink(candidateLink)
- return true
- false
-
-findAndFollowRel = (value) ->
- relTags = ["link", "a", "area"]
- for tag in relTags
- elements = document.getElementsByTagName(tag)
- for element in elements
- if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
- followLink(element)
- return true
-
-root.goPrevious = ->
- previousPatterns = Settings.get("previousPatterns") || ""
- previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("prev") || findAndFollowLink(previousStrings)
-
-root.goNext = ->
- nextPatterns = Settings.get("nextPatterns") || ""
- nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("next") || findAndFollowLink(nextStrings)
-
-# Enters find mode. Returns the new find-mode instance.
-enterFindMode = ->
- Marks.setPreviousPosition()
- new FindMode()
-
-root.showHelp = (sourceFrameId) ->
- HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
-
# If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions.
root.HelpDialog ?=
helpUI: null
@@ -704,9 +338,8 @@ root.frameId = frameId
root.Frame = Frame
root.windowIsFocused = windowIsFocused
root.bgLog = bgLog
-# These are exported for find mode and link-hints mode.
-extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind,
- enterFindMode, focusThisFrame}
+# These are exported for normal mode and link-hints mode.
+extend root, {focusThisFrame}
# These are exported only for the tests.
extend root, {installModes}
extend window, root unless exports?
diff --git a/lib/utils.coffee b/lib/utils.coffee
index b5b96844..6f38be8f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -32,14 +32,6 @@ Utils =
# Returns true whenever the current page is the extension's background page.
isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage?() == window
- # Takes a dot-notation object string and calls the function that it points to with the correct value for
- # 'this'.
- invokeCommandString: (str, args...) ->
- [names..., name] = str.split '.'
- obj = window
- obj = obj[component] for component in names
- obj[name].apply obj, args
-
# Escape all special characters, so RegExp will parse the string 'as is'.
# Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
escapeRegexSpecialCharacters: do ->
diff --git a/manifest.json b/manifest.json
index f90278b7..71d7caba 100644
--- a/manifest.json
+++ b/manifest.json
@@ -60,6 +60,7 @@
"content_scripts/mode_key_handler.js",
"content_scripts/mode_visual.js",
"content_scripts/hud.js",
+ "content_scripts/mode_normal.js",
"content_scripts/vimium_frontend.js"
],
"css": ["content_scripts/vimium.css"],
diff --git a/pages/blank.html b/pages/blank.html
index 8f10c7f6..d026912e 100644
--- a/pages/blank.html
+++ b/pages/blank.html
@@ -19,6 +19,7 @@
<script src="../content_scripts/mode_key_handler.js"></script>
<script src="../content_scripts/mode_visual.js"></script>
<script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
<script src="../content_scripts/vimium_frontend.js"></script>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
diff --git a/pages/completion_engines.html b/pages/completion_engines.html
index 0c86edf7..3313b26c 100644
--- a/pages/completion_engines.html
+++ b/pages/completion_engines.html
@@ -22,6 +22,7 @@
<script src="../content_scripts/mode_key_handler.js"></script>
<script src="../content_scripts/mode_visual.js"></script>
<script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
<script src="../content_scripts/vimium_frontend.js"></script>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 1da54efd..7f053265 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -19,6 +19,7 @@
<script src="../content_scripts/mode_key_handler.js"></script>
<script src="../content_scripts/mode_visual.js"></script>
<script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
<script src="../content_scripts/vimium_frontend.js"></script>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
diff --git a/pages/logging.html b/pages/logging.html
index 6eff58c4..17aafd70 100644
--- a/pages/logging.html
+++ b/pages/logging.html
@@ -19,6 +19,7 @@
<script src="../content_scripts/mode_key_handler.js"></script>
<script src="../content_scripts/mode_visual.js"></script>
<script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
<script src="../content_scripts/vimium_frontend.js"></script>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
diff --git a/pages/options.html b/pages/options.html
index f14ac3df..bfdb9b53 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -20,6 +20,7 @@
<script src="../content_scripts/mode_key_handler.js"></script>
<script src="../content_scripts/mode_visual.js"></script>
<script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
<script src="../content_scripts/vimium_frontend.js"></script>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index a5c85606..6e422d46 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -436,29 +436,29 @@ context "Input focus",
document.getElementById("test-div").innerHTML = ""
should "focus the first element", ->
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.equal "first", document.activeElement.id
should "focus the nth element", ->
- focusInput 100
+ NormalModeCommands.focusInput 100
assert.equal "third", document.activeElement.id
should "activate insert mode on the first element", ->
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.isTrue InsertMode.permanentInstance.isActive()
should "activate insert mode on the first element", ->
- focusInput 100
+ NormalModeCommands.focusInput 100
assert.isTrue InsertMode.permanentInstance.isActive()
should "activate the most recently-selected input if the count is 1", ->
- focusInput 3
- focusInput 1
+ NormalModeCommands.focusInput 3
+ NormalModeCommands.focusInput 1
assert.equal "third", document.activeElement.id
should "not trigger insert if there are no inputs", ->
document.getElementById("test-div").innerHTML = ""
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.isFalse InsertMode.permanentInstance.isActive()
# TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has
@@ -479,7 +479,7 @@ context "Find prev / next links",
<a href='#second'>next page</a>
"""
stubSettings "nextPatterns", "next"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#second', window.location.hash
should "match against non-word patterns", ->
@@ -487,7 +487,7 @@ context "Find prev / next links",
<a href='#first'>&gt;&gt;</a>
"""
stubSettings "nextPatterns", ">>"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "favor matches with fewer words", ->
@@ -496,14 +496,14 @@ context "Find prev / next links",
<a href='#second'>next!</a>
"""
stubSettings "nextPatterns", "next"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#second', window.location.hash
should "find link relation in header", ->
document.getElementById("test-div").innerHTML = """
<link rel='next' href='#first'>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "favor link relation to text matching", ->
@@ -511,14 +511,14 @@ context "Find prev / next links",
<link rel='next' href='#first'>
<a href='#second'>next</a>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "match mixed case link relation", ->
document.getElementById("test-div").innerHTML = """
<link rel='Next' href='#first'>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
createLinks = (n) ->
diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html
index d2e795d1..37cd43e3 100644
--- a/tests/dom_tests/dom_tests.html
+++ b/tests/dom_tests/dom_tests.html
@@ -49,6 +49,7 @@
<script type="text/javascript" src="../../content_scripts/mode_key_handler.js"></script>
<script type="text/javascript" src="../../content_scripts/mode_visual.js"></script>
<script type="text/javascript" src="../../content_scripts/hud.js"></script>
+ <script type="text/javascript" src="../../content_scripts/mode_normal.js"></script>
<script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script>
<script type="text/javascript" src="../shoulda.js/shoulda.js"></script>
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index 0e0be1d6..49dd2570 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -4,6 +4,14 @@ extend global, require "../../background_scripts/bg_utils.js"
global.Settings = {postUpdateHooks: {}, get: (-> ""), set: ->}
{Commands} = require "../../background_scripts/commands.js"
+# Include mode_normal to check that all commands have been implemented.
+global.KeyHandlerMode = global.Mode = {}
+global.KeyboardUtils = {platform: ""}
+extend global, require "../../content_scripts/link_hints.js"
+extend global, require "../../content_scripts/marks.js"
+extend global, require "../../content_scripts/vomnibar.js"
+{NormalModeCommands} = require "../../content_scripts/mode_normal.js"
+
context "Key mappings",
setup ->
@testKeySequence = (key, expectedKeyText, expectedKeyLength) ->
@@ -114,6 +122,14 @@ context "Parse commands",
assert.equal "a", BgUtils.parseLines(" a \n b")[0]
assert.equal "b", BgUtils.parseLines(" a \n b")[1]
-# TODO (smblott) More tests:
-# - Ensure each background command has an implmentation in BackgroundCommands
-# - Ensure each foreground command has an implmentation in vimium_frontent.coffee
+context "Commands implemented",
+ (for own command, options of Commands.availableCommands
+ do (command, options) ->
+ if options.background
+ should "#{command} (background command)", ->
+ # TODO: Import background_scripts/main.js and expose BackgroundCommands from there.
+ # assert.isTrue BackgroundCommands[command]
+ else
+ should "#{command} (foreground command)", ->
+ assert.isTrue NormalModeCommands[command]
+ )...
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index cc1081dd..2794a6d7 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -138,26 +138,6 @@ context "distinctCharacters",
should "eliminate duplicate characters", ->
assert.equal "abc", Utils.distinctCharacters "bbabaabbacabbbab"
-context "invokeCommandString",
- setup ->
- @beenCalled = false
- window.singleComponentCommand = => @beenCalled = true
- window.twoComponentCommand = command: window.singleComponentCommand
-
- tearDown ->
- delete window.singleComponentCommand
- delete window.twoComponentCommand
-
- should "invoke single-component commands", ->
- assert.isFalse @beenCalled
- Utils.invokeCommandString "singleComponentCommand"
- assert.isTrue @beenCalled
-
- should "invoke multi-component commands", ->
- assert.isFalse @beenCalled
- Utils.invokeCommandString "twoComponentCommand.command"
- assert.isTrue @beenCalled
-
context "escapeRegexSpecialCharacters",
should "escape regexp special characters", ->
str = "-[]/{}()*+?.^$|"