diff options
| -rw-r--r-- | content_scripts/hud.coffee | 40 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 156 | ||||
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 29 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 16 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 288 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 12 | ||||
| -rw-r--r-- | lib/find_mode_history.coffee | 50 | ||||
| -rw-r--r-- | manifest.json | 1 | ||||
| -rw-r--r-- | pages/hud.coffee | 87 | ||||
| -rw-r--r-- | pages/hud.html | 3 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.html | 1 | 
11 files changed, 395 insertions, 288 deletions
| diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index 84b8abeb..bfad71b7 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -6,6 +6,7 @@ HUD =    tween: null    hudUI: null    _displayElement: null +  findMode: null    # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"    # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that @@ -26,6 +27,19 @@ HUD =      @hudUI.show {name: "show", text}      @tween.fade 1.0, 150 +  showFindMode: (@findMode = null) -> +    return unless @enabled() +    @hudUI.show {name: "showFindMode", text: ""} +    @tween.fade 1.0, 150 + +  search: (data) -> +    @findMode.findInPlace data.query + +    # Show the number of matches in the HUD UI. +    matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0 +    showMatchText = FindMode.query.rawQuery.length > 0 +    @hudUI.postMessage {name: "updateMatchesCount", matchCount, showMatchText} +    # Hide the HUD.    # If :immediate is falsy, then the HUD is faded out smoothly (otherwise it is hidden immediately).    # If :updateIndicator is truthy, then we also refresh the mode indicator.  The only time we don't update the @@ -42,6 +56,32 @@ HUD =      else        @tween.fade 0, 150, => @hide true, updateIndicator +  hideFindMode: (data) -> +    @findMode.checkReturnToViewPort() + +    # An element element won't receive a focus event if the search landed on it while we were in the HUD +    # iframe. To end up with the correct modes active, we create a focus/blur event manually after refocusing +    # this window. +    window.focus() + +    focusNode = DomUtils.getSelectionFocusElement() +    document.activeElement?.blur() +    focusNode?.focus() + +    {event} = data + +    if event.keyCode == keyCodes.enter +      handleEnterForFindMode() +      if FindMode.query.hasResults +        postExit = -> new PostFindMode +    else if KeyboardUtils.isEscape event +      # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we +      # wait until the mode is closed before running it. +      postExit = handleEscapeForFindMode + +    @findMode.exit() +    postExit?() +    isReady: do ->      ready = false      DomUtils.documentReady -> ready = true diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index ed08fbd5..d7c628be 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -54,5 +54,161 @@ class PostFindMode extends SuppressPrintable            handlerStack.remove()            @continueBubbling +class FindMode extends Mode +  @query: +    rawQuery: "" +    matchCount: 0 +    hasResults: false + +  constructor: (options = {}) -> +    # Save the selection, so findInPlace can restore it. +    @initialRange = getCurrentRange() +    FindMode.query = rawQuery: "" +    if options.returnToViewport +      @scrollX = window.scrollX +      @scrollY = window.scrollY +    super extend options, +      name: "find" +      indicator: false +      exitOnClick: true + +    HUD.showFindMode this + +  exit: (event) -> +    super() +    handleEscapeForFindMode() if event + +  restoreSelection: -> +    range = @initialRange +    selection = getSelection() +    selection.removeAllRanges() +    selection.addRange range + +  findInPlace: (query) -> +    # If requested, restore the scroll position (so that failed searches leave the scroll position unchanged). +    @checkReturnToViewPort() +    FindMode.updateQuery query +    # Restore the selection.  That way, we're always searching forward from the same place, so we find the right +    # match as the user adds matching characters, or removes previously-matched characters. See #1434. +    @restoreSelection() +    query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery +    FindMode.query.hasResults = FindMode.execute query + +  @updateQuery: (query) -> +    @query.rawQuery = query +    # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of +    # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal +    # character. here we grep for the relevant escape sequences. +    @query.isRegex = Settings.get 'regexFindMode' +    hasNoIgnoreCaseFlag = false +    @query.parsedQuery = @query.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) -> +      return match if flag == "" or slashes.length != 1 +      switch (flag) +        when "r" +          @query.isRegex = true +        when "R" +          @query.isRegex = false +        when "I" +          hasNoIgnoreCaseFlag = true +      "" + +    # default to 'smartcase' mode, unless noIgnoreCase is explicitly specified +    @query.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(@query.parsedQuery) + +    # 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 @query.isRegex +      try +        pattern = new RegExp(@query.parsedQuery, "g" + (if @query.ignoreCase then "i" else "")) +      catch error +        # if we catch a SyntaxError, assume the user is not done typing yet and return quietly +        return +      # innerText will not return the text of hidden elements, and strip out tags while preserving newlines +      text = document.body.innerText +      @query.regexMatches = text.match(pattern) +      @query.activeRegexIndex = 0 +      @query.matchCount = @query.regexMatches?.length +    # if we are doing a basic plain string match, we still want to grep for matches of the string, so we can +    # show a the number of results. We can grep on document.body.innerText, as it should be indistinguishable +    # from the internal representation used by window.find. +    else +      # escape all special characters, so RegExp just parses the string 'as is'. +      # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex +      escapeRegExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g +      parsedNonRegexQuery = @query.parsedQuery.replace(escapeRegExp, (char) -> "\\" + char) +      pattern = new RegExp(parsedNonRegexQuery, "g" + (if @query.ignoreCase then "i" else "")) +      text = document.body.innerText +      @query.matchCount = text.match(pattern)?.length + +  @getNextQueryFromRegexMatches: (stepSize) -> +    # find()ing an empty query always returns false +    return "" unless @query.regexMatches + +    totalMatches = @query.regexMatches.length +    @query.activeRegexIndex += stepSize + totalMatches +    @query.activeRegexIndex %= totalMatches + +    @query.regexMatches[@query.activeRegexIndex] + +  @getQuery: (backwards) -> +    # check if the query has been changed by a script in another frame +    mostRecentQuery = FindModeHistory.getQuery() +    if (mostRecentQuery != @query.rawQuery) +      @updateQuery mostRecentQuery + +    if @query.isRegex +      @getNextQueryFromRegexMatches(if backwards then -1 else 1) +    else +      @query.parsedQuery + +  @saveQuery: -> FindModeHistory.saveQuery @query.rawQuery + +  # :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'. +  @execute: (query, options) -> +    result = null +    options = extend { +      backwards: false +      caseSensitive: !@query.ignoreCase +      colorSelection: true +    }, options +    query ?= FindMode.getQuery options.backwards + +    if options.colorSelection +      document.body.classList.add("vimiumFindMode") +      # ignore the selectionchange event generated by find() +      document.removeEventListener("selectionchange", @restoreDefaultSelectionHighlight, true) + +    result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) + +    if options.colorSelection +      setTimeout( +        -> document.addEventListener("selectionchange", @restoreDefaultSelectionHighlight, true) +      , 0) + +    # We are either in normal mode ("n"), or find mode ("/").  We are not in insert mode.  Nevertheless, if a +    # previous find landed in an editable element, then that element may still be activated.  In this case, we +    # don't want to leave it behind (see #1412). +    if document.activeElement and DomUtils.isEditable document.activeElement +      document.activeElement.blur() unless DomUtils.isSelected document.activeElement + +    result + +  @restoreDefaultSelectionHighlight: -> document.body.classList.remove("vimiumFindMode") + +  checkReturnToViewPort: -> +    window.scrollTo @scrollX, @scrollY if @options.returnToViewport + +getCurrentRange = -> +  selection = getSelection() +  if selection.type == "None" +    range = document.createRange() +    range.setStart document.body, 0 +    range.setEnd document.body, 0 +    range +  else +    selection.collapseToStart() if selection.type == "Range" +    selection.getRangeAt 0 +  root = exports ? window  root.PostFindMode = PostFindMode +root.FindMode = FindMode diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 8d1d96cc..eaaf94d4 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -351,22 +351,21 @@ class Movement extends CountPrefix      # element), or if this instance has been created to execute only a single movement.      unless @options.parentMode or options.oneMovementOnly        do => -        executeFind = (count, findBackwards) => -          if query = getFindModeQuery findBackwards -            initialRange = @selection.getRangeAt(0).cloneRange() -            for [0...count] -              unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false -                @setSelectionRange initialRange -                HUD.showForDuration("No matches for '" + query + "'", 1000) -                return -            # The find was successfull. If we're in caret mode, then we should now have a selection, so we can -            # drop back into visual mode. -            @changeMode VisualMode if @name == "caret" and 0 < @selection.toString().length - -        @movements.n = (count) -> executeFind count, false -        @movements.N = (count) -> executeFind count, true +        doFind = (count, backwards) => +          initialRange = @selection.getRangeAt(0).cloneRange() +          for [0...count] by 1 +            unless FindMode.execute null, {colorSelection: false, backwards} +              @setSelectionRange initialRange +              HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) +              return +          # The find was successfull. If we're in caret mode, then we should now have a selection, so we can +          # drop back into visual mode. +          @changeMode VisualMode if @name == "caret" and 0 < @selection.toString().length + +        @movements.n = (count) -> doFind count, false +        @movements.N = (count) -> doFind count, true          @movements["/"] = -> -          @findMode = window.enterFindMode returnToViewport: true +          @findMode = new FindMode returnToViewport: true            @findMode.onExit => @changeMode VisualMode      #      # End of Movement constructor. diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 38a968fc..e02df7c2 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -284,6 +284,22 @@ iframe.vimiumHUDFrame {    opacity: 0;  } +div.vimiumHUD span#hud-find-input, div.vimiumHUD span#hud-match-count { +  display: inline; +  outline: none; +  white-space: nowrap; +  overflow-y: hidden; +} + +div.vimiumHUD span#hud-find-input br { +  display: none; +} + +div.vimiumHUD span#hud-find-input * { +  display: inline; +  white-space: nowrap; +} +  body.vimiumFindMode ::selection {    background: #ff9632;  } diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 8c28b4e6..6f50fb9d 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -5,10 +5,6 @@  # "domReady".  # -findModeQuery = { rawQuery: "", matchCount: 0 } -findModeQueryHasResults = false -findModeAnchorNode = null -findModeInitialRange = null  isShowingHelpDialog = false  keyPort = null  isEnabledForUrl = true @@ -622,108 +618,7 @@ window.refreshCompletionKeys = (response) ->  isValidFirstKey = (keyChar) ->    validFirstKeys[keyChar] || /^[1-9]/.test(keyChar) -# This implements find-mode query history (using the "findModeRawQueryList" setting) as a list of raw queries, -# most recent first. -FindModeHistory = -  storage: chrome.storage.local -  key: "findModeRawQueryList" -  max: 50 -  rawQueryList: null - -  init: -> -    unless @rawQueryList -      @rawQueryList = [] # Prevent repeated initialization. -      @key = "findModeRawQueryListIncognito" if isIncognitoMode -      @storage.get @key, (items) => -        unless chrome.runtime.lastError -          @rawQueryList = items[@key] if items[@key] -          if isIncognitoMode and not items[@key] -            # This is the first incognito tab, so we need to initialize the incognito-mode query history. -            @storage.get "findModeRawQueryList", (items) => -              unless chrome.runtime.lastError -                @rawQueryList = items.findModeRawQueryList -                @storage.set findModeRawQueryListIncognito: @rawQueryList - -    chrome.storage.onChanged.addListener (changes, area) => -      @rawQueryList = changes[@key].newValue if changes[@key] - -  getQuery: (index = 0) -> -    @rawQueryList[index] or "" - -  saveQuery: (query) -> -    if 0 < query.length -      @rawQueryList = @refreshRawQueryList query, @rawQueryList -      newSetting = {}; newSetting[@key] = @rawQueryList -      @storage.set newSetting -      # If there are any active incognito-mode tabs, then propagte this query to those tabs too. -      unless isIncognitoMode -        @storage.get "findModeRawQueryListIncognito", (items) => -          if not chrome.runtime.lastError and items.findModeRawQueryListIncognito -            @storage.set -              findModeRawQueryListIncognito: @refreshRawQueryList query, items.findModeRawQueryListIncognito - -  refreshRawQueryList: (query, rawQueryList) -> -    ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max] - -# should be called whenever rawQuery is modified. -updateFindModeQuery = -> -  # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of -  # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal -  # character. here we grep for the relevant escape sequences. -  findModeQuery.isRegex = Settings.get 'regexFindMode' -  hasNoIgnoreCaseFlag = false -  findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) -> -    return match if flag == "" or slashes.length != 1 -    switch (flag) -      when "r" -        findModeQuery.isRegex = true -      when "R" -        findModeQuery.isRegex = false -      when "I" -        hasNoIgnoreCaseFlag = true -    "" - -  # 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 -    try -      pattern = new RegExp(findModeQuery.parsedQuery, "g" + (if findModeQuery.ignoreCase then "i" else "")) -    catch error -      # if we catch a SyntaxError, assume the user is not done typing yet and return quietly -      return -    # innerText will not return the text of hidden elements, and strip out tags while preserving newlines -    text = document.body.innerText -    findModeQuery.regexMatches = text.match(pattern) -    findModeQuery.activeRegexIndex = 0 -    findModeQuery.matchCount = findModeQuery.regexMatches?.length -  # if we are doing a basic plain string match, we still want to grep for matches of the string, so we can -  # show a the number of results. We can grep on document.body.innerText, as it should be indistinguishable -  # from the internal representation used by window.find. -  else -    # escape all special characters, so RegExp just parses the string 'as is'. -    # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex -    escapeRegExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g -    parsedNonRegexQuery = findModeQuery.parsedQuery.replace(escapeRegExp, (char) -> "\\" + char) -    pattern = new RegExp(parsedNonRegexQuery, "g" + (if findModeQuery.ignoreCase then "i" else "")) -    text = document.body.innerText -    findModeQuery.matchCount = text.match(pattern)?.length - -updateQueryForFindMode = (rawQuery) -> -  findModeQuery.rawQuery = rawQuery -  updateFindModeQuery() -  performFindInPlace() -  showFindModeHUDForQuery() - -handleKeyCharForFindMode = (keyChar) -> -  updateQueryForFindMode findModeQuery.rawQuery + keyChar - -handleEscapeForFindMode = -> +window.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. @@ -734,158 +629,38 @@ handleEscapeForFindMode = ->      window.getSelection().addRange(range)    focusFoundLink() || selectFoundInputElement() -# Return true if character deleted, false otherwise. -handleDeleteForFindMode = -> -  if findModeQuery.rawQuery.length == 0 -    HUD.hide() -    false -  else -    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  # this query and do more searches with it' -handleEnterForFindMode = -> +window.handleEnterForFindMode = ->    focusFoundLink()    document.body.classList.add("vimiumFindMode") -  FindModeHistory.saveQuery findModeQuery.rawQuery - -class FindMode extends Mode -  constructor: (options = {}) -> -    @historyIndex = -1 -    @partialQuery = "" -    if options.returnToViewport -      @scrollX = window.scrollX -      @scrollY = window.scrollY -    super -      name: "find" -      indicator: false -      exitOnEscape: true -      exitOnClick: true - -      keydown: (event) => -        window.scrollTo @scrollX, @scrollY if options.returnToViewport -        if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey -          @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 - -      keypress: (event) => -        handlerStack.neverContinueBubbling => -          if event.keyCode > 31 -            keyChar = String.fromCharCode event.charCode -            handleKeyCharForFindMode keyChar if keyChar - -      keyup: (event) => @suppressEvent - -  exit: (event) -> -    super() -    handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event -    handleEscapeForFindMode() if event?.type == "click" -    if findModeQueryHasResults and event?.type != "click" -      new PostFindMode - -performFindInPlace = -> -  # Restore the selection.  That way, we're always searching forward from the same place, so we find the right -  # match as the user adds matching characters, or removes previously-matched characters. See #1434. -  findModeRestoreSelection() -  query = if findModeQuery.isRegex then getNextQueryFromRegexMatches(0) else findModeQuery.parsedQuery -  findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase }) - -# :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'. -executeFind = (query, options) -> -  result = null -  options = options || {} - -  document.body.classList.add("vimiumFindMode") - -  # ignore the selectionchange event generated by find() -  document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) -  result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) -  setTimeout( -    -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) -    0) - -  # We are either in normal mode ("n"), or find mode ("/").  We are not in insert mode.  Nevertheless, if a -  # previous find landed in an editable element, then that element may still be activated.  In this case, we -  # don't want to leave it behind (see #1412). -  if document.activeElement and DomUtils.isEditable document.activeElement -    document.activeElement.blur() unless DomUtils.isSelected document.activeElement - -  # we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do -  # preventDefault() -  findModeAnchorNode = document.getSelection().anchorNode -  result - -restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode") +  FindMode.saveQuery()  focusFoundLink = -> -  if (findModeQueryHasResults) +  if (FindMode.query.hasResults)      link = getLinkFromSelection()      link.focus() if link  selectFoundInputElement = -> -  # if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement -  # instead. however, 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. -  if (findModeQueryHasResults && document.activeElement && +  # 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) -getNextQueryFromRegexMatches = (stepSize) -> -  # find()ing an empty query always returns false -  return "" unless findModeQuery.regexMatches - -  totalMatches = findModeQuery.regexMatches.length -  findModeQuery.activeRegexIndex += stepSize + totalMatches -  findModeQuery.activeRegexIndex %= totalMatches - -  findModeQuery.regexMatches[findModeQuery.activeRegexIndex] - -window.getFindModeQuery = (backwards) -> -  # check if the query has been changed by a script in another frame -  mostRecentQuery = FindModeHistory.getQuery() -  if (mostRecentQuery != findModeQuery.rawQuery) -    findModeQuery.rawQuery = mostRecentQuery -    updateFindModeQuery() - -  if findModeQuery.isRegex -    getNextQueryFromRegexMatches(if backwards then -1 else 1) -  else -    findModeQuery.parsedQuery -  findAndFocus = (backwards) ->    Marks.setPreviousPosition() -  query = getFindModeQuery backwards - -  findModeQueryHasResults = -    executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase }) +  FindMode.query.hasResults = FindMode.execute null, {backwards} -  if findModeQueryHasResults +  if FindMode.query.hasResults      focusFoundLink() -    new PostFindMode() if findModeQueryHasResults +    new PostFindMode()    else -    HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000) +    HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)  window.performFind = -> findAndFocus() @@ -992,43 +767,10 @@ window.goNext = ->    nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )    findAndFollowRel("next") || findAndFollowLink(nextStrings) -showFindModeHUDForQuery = -> -  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() -  if selection.type == "None" -    range = document.createRange() -    range.setStart document.body, 0 -    range.setEnd document.body, 0 -    range -  else -    selection.collapseToStart() if selection.type == "Range" -    selection.getRangeAt 0 - -findModeSaveSelection = -> -  findModeInitialRange = getCurrentRange() - -findModeRestoreSelection = (range = findModeInitialRange) -> -  selection = getSelection() -  selection.removeAllRanges() -  selection.addRange range -  # Enters find mode.  Returns the new find-mode instance. -window.enterFindMode = (options = {}) -> +window.enterFindMode = ->    Marks.setPreviousPosition() -  # Save the selection, so performFindInPlace can restore it. -  findModeSaveSelection() -  findModeQuery = rawQuery: "" -  findMode = new FindMode options -  HUD.show "/" -  findMode +  new FindMode()  window.showHelpDialog = (html, fid) ->    return if (isShowingHelpDialog || !document.body || fid != frameId) diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 7c47179c..9658df2b 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -326,6 +326,18 @@ DomUtils =        document.body.removeChild div        coordinates +  getSelectionFocusElement: -> +    sel = window.getSelection() +    if not sel.focusNode? +      null +    else if sel.focusNode == sel.anchorNode and sel.focusOffset == sel.anchorOffset +      # The selection either *is* an element, or is inside an opaque element (eg. <input>). +      sel.focusNode.childNodes[sel.focusOffset] +    else if sel.focusNode.nodeType != sel.focusNode.ELEMENT_NODE +      sel.focusNode.parentElement +    else +      sel.focusNode +    # Get the text content of an element (and its descendents), but omit the text content of previously-visited    # nodes.  See #1514.    # NOTE(smblott).  This is currently O(N^2) (when called on N elements).  An alternative would be to mark diff --git a/lib/find_mode_history.coffee b/lib/find_mode_history.coffee new file mode 100644 index 00000000..ff660bd2 --- /dev/null +++ b/lib/find_mode_history.coffee @@ -0,0 +1,50 @@ +# NOTE(mrmr1993): This is under lib/ since it is used by both content scripts and iframes from pages/. +# This implements find-mode query history (using the "findModeRawQueryList" setting) as a list of raw queries, +# most recent first. +FindModeHistory = +  storage: chrome?.storage.local # Guard against chrome being undefined (in the HUD iframe). +  key: "findModeRawQueryList" +  max: 50 +  rawQueryList: null + +  init: -> +    @isIncognitoMode = chrome?.extension.inIncognitoContext + +    return unless @isIncognitoMode? # chrome is undefined in the HUD iframe during tests, so we do nothing. + +    unless @rawQueryList +      @rawQueryList = [] # Prevent repeated initialization. +      @key = "findModeRawQueryListIncognito" if @isIncognitoMode +      @storage.get @key, (items) => +        unless chrome.runtime.lastError +          @rawQueryList = items[@key] if items[@key] +          if @isIncognitoMode and not items[@key] +            # This is the first incognito tab, so we need to initialize the incognito-mode query history. +            @storage.get "findModeRawQueryList", (items) => +              unless chrome.runtime.lastError +                @rawQueryList = items.findModeRawQueryList +                @storage.set findModeRawQueryListIncognito: @rawQueryList + +    chrome.storage.onChanged.addListener (changes, area) => +      @rawQueryList = changes[@key].newValue if changes[@key] + +  getQuery: (index = 0) -> +    @rawQueryList[index] or "" + +  saveQuery: (query) -> +    if 0 < query.length +      @rawQueryList = @refreshRawQueryList query, @rawQueryList +      newSetting = {}; newSetting[@key] = @rawQueryList +      @storage.set newSetting +      # If there are any active incognito-mode tabs, then propagte this query to those tabs too. +      unless @isIncognitoMode +        @storage.get "findModeRawQueryListIncognito", (items) => +          if not chrome.runtime.lastError and items.findModeRawQueryListIncognito +            @storage.set +              findModeRawQueryListIncognito: @refreshRawQueryList query, items.findModeRawQueryListIncognito + +  refreshRawQueryList: (query, rawQueryList) -> +    ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max] + +root = exports ? window +root.FindModeHistory = FindModeHistory diff --git a/manifest.json b/manifest.json index 80aca4c5..4ef5edfe 100644 --- a/manifest.json +++ b/manifest.json @@ -42,6 +42,7 @@               "lib/handler_stack.js",               "lib/clipboard.js",               "lib/settings.js", +             "lib/find_mode_history.js",               "content_scripts/ui_component.js",               "content_scripts/link_hints.js",               "content_scripts/vomnibar.js", diff --git a/pages/hud.coffee b/pages/hud.coffee index 68283451..37debc4e 100644 --- a/pages/hud.coffee +++ b/pages/hud.coffee @@ -1,3 +1,49 @@ +findMode = null + +# Set the input element's text, and move the cursor to the end. +setTextInInputElement = (inputElement, text) -> +  inputElement.textContent = text +  # Move the cursor to the end.  Based on one of the solutions here: +  # http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity +  range = document.createRange() +  range.selectNodeContents inputElement +  range.collapse false +  selection = window.getSelection() +  selection.removeAllRanges() +  selection.addRange range + +document.addEventListener "keydown", (event) -> +  inputElement = document.getElementById "hud-find-input" +  return unless inputElement? # Don't do anything if we're not in find mode. +  transferrableEvent = {} +  for key, value of event +    transferrableEvent[key] = value if typeof value in ["number", "string"] + +  if (event.keyCode in [keyCodes.backspace, keyCodes.deleteKey] and inputElement.textContent.length == 0) or +     event.keyCode == keyCodes.enter or KeyboardUtils.isEscape event + +    UIComponentServer.postMessage +      name: "hideFindMode" +      event: transferrableEvent +      query: findMode.rawQuery + +  else if event.keyCode == keyCodes.upArrow +    if rawQuery = FindModeHistory.getQuery findMode.historyIndex + 1 +      findMode.historyIndex += 1 +      findMode.partialQuery = findMode.rawQuery if findMode.historyIndex == 0 +      setTextInInputElement inputElement, rawQuery +      findMode.executeQuery() +  else if event.keyCode == keyCodes.downArrow +    findMode.historyIndex = Math.max -1, findMode.historyIndex - 1 +    rawQuery = if 0 <= findMode.historyIndex then FindModeHistory.getQuery findMode.historyIndex else findMode.partialQuery +    setTextInInputElement inputElement, rawQuery +    findMode.executeQuery() +  else +    return + +  DomUtils.suppressEvent event +  false +  handlers =    show: (data) ->      document.getElementById("hud").innerText = data.text @@ -10,6 +56,47 @@ handlers =      document.getElementById("hud").classList.add "vimiumUIComponentHidden"      document.getElementById("hud").classList.remove "vimiumUIComponentVisible" +  showFindMode: (data) -> +    hud = document.getElementById "hud" +    hud.innerText = "/\u200A" # \u200A is a "hair space", to leave enough space before the caret/first char. + +    inputElement = document.createElement "span" +    inputElement.contentEditable = "plaintext-only" +    setTextInInputElement inputElement, data.text ? "" +    inputElement.id = "hud-find-input" +    hud.appendChild inputElement + +    inputElement.addEventListener "input", executeQuery = (event) -> +      # Replace \u00A0 ( ) with a normal space. +      findMode.rawQuery = inputElement.textContent.replace "\u00A0", " " +      UIComponentServer.postMessage {name: "search", query: findMode.rawQuery} + +    countElement = document.createElement "span" +    countElement.id = "hud-match-count" +    hud.appendChild countElement +    inputElement.focus() + +    # Replace \u00A0 ( ) with a normal space. +    UIComponentServer.postMessage {name: "search", query: inputElement.textContent.replace "\u00A0", " "} + +    findMode = +      historyIndex: -1 +      partialQuery: "" +      rawQuery: "" +      executeQuery: executeQuery + +  updateMatchesCount: ({matchCount, showMatchText}) -> +    countElement = document.getElementById "hud-match-count" +    return unless countElement? # Don't do anything if we're not in find mode. + +    countText = if matchCount > 0 +      " (#{matchCount} Match#{if matchCount == 1 then "" else "es"})" +    else +      " (No matches)" +    countElement.textContent = if showMatchText then countText else "" +  UIComponentServer.registerHandler (event) ->    {data} = event    handlers[data.name]? data + +FindModeHistory.init() diff --git a/pages/hud.html b/pages/hud.html index bcb38e04..60d737e1 100644 --- a/pages/hud.html +++ b/pages/hud.html @@ -2,6 +2,9 @@    <head>      <title>HUD</title>      <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> +    <script type="text/javascript" src="../lib/dom_utils.js"></script> +    <script type="text/javascript" src="../lib/keyboard_utils.js"></script> +    <script type="text/javascript" src="../lib/find_mode_history.js"></script>      <script type="text/javascript" src="ui_component_server.js"></script>      <script type="text/javascript" src="hud.js"></script>    </head> diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index f7cc430d..25c5f8ba 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -36,6 +36,7 @@      <script type="text/javascript" src="../../lib/handler_stack.js"></script>      <script type="text/javascript" src="../../lib/clipboard.js"></script>      <script type="text/javascript" src="../../lib/settings.js"></script> +    <script type="text/javascript" src="../../lib/find_mode_history.js"></script>      <script type="text/javascript" src="../../content_scripts/ui_component.js"></script>      <script type="text/javascript" src="../../content_scripts/link_hints.js"></script>      <script type="text/javascript" src="../../content_scripts/vomnibar.js"></script> | 
