diff options
| -rw-r--r-- | background_scripts/main.coffee | 60 | ||||
| -rw-r--r-- | background_scripts/marks.coffee | 2 | ||||
| -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 | 35 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 16 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 292 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 12 | ||||
| -rw-r--r-- | lib/find_mode_history.coffee | 50 | ||||
| -rw-r--r-- | lib/settings.coffee | 88 | ||||
| -rw-r--r-- | lib/utils.coffee | 9 | ||||
| -rw-r--r-- | manifest.json | 1 | ||||
| -rw-r--r-- | pages/hud.coffee | 87 | ||||
| -rw-r--r-- | pages/hud.html | 3 | ||||
| -rw-r--r-- | pages/options.coffee | 67 | ||||
| -rw-r--r-- | pages/options.html | 2 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.html | 1 | ||||
| -rw-r--r-- | tests/unit_tests/completion_test.coffee | 3 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 20 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 11 | 
20 files changed, 551 insertions, 404 deletions
| diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index d4b14f3c..40d570ee 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -187,37 +187,35 @@ getCompletionKeysRequest = (request, keysToCheck = "") ->    completionKeys: generateCompletionKeys(keysToCheck)    validFirstKeys: validFirstKeys -# -# Opens the url in the current tab. -# -openUrlInCurrentTab = (request) -> -  chrome.tabs.getSelected(null, -    (tab) -> chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) })) +TabOperations = +  # Opens the url in the current tab. +  openUrlInCurrentTab: (request, callback = (->)) -> +    chrome.tabs.getSelected null, (tab) -> +      callback = (->) unless typeof callback == "function" +      chrome.tabs.update tab.id, { url: Utils.convertToUrl(request.url) }, callback -# -# Opens request.url in new tab and switches to it if request.selected is true. -# -openUrlInNewTab = (request, callback) -> -  chrome.tabs.getSelected null, (tab) -> -    tabConfig = -      url: Utils.convertToUrl request.url -      index: tab.index + 1 -      selected: true -      windowId: tab.windowId -    # FIXME(smblott). openUrlInNewTab is being called in two different ways with different arguments.  We -    # should refactor it such that this check on callback isn't necessary. +  # Opens request.url in new tab and switches to it if request.selected is true. +  openUrlInNewTab: (request, callback = (->)) -> +    chrome.tabs.getSelected null, (tab) -> +      tabConfig = +        url: Utils.convertToUrl request.url +        index: tab.index + 1 +        selected: true +        windowId: tab.windowId +      openerTabId: tab.id +      callback = (->) unless typeof callback == "function" +      chrome.tabs.create tabConfig, callback + +  openUrlInIncognito: (request, callback = (->)) ->      callback = (->) unless typeof callback == "function" -    chrome.tabs.create tabConfig, callback - -openUrlInIncognito = (request) -> -  chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true}) +    chrome.windows.create {url: Utils.convertToUrl(request.url), incognito: true}, callback  #  # Copies or pastes some data (request.data) to/from the clipboard.  # We return null to avoid the return value from the copy operations being passed to sendResponse.  #  copyToClipboard = (request) -> Clipboard.copy(request.data); null -pasteFromClipboard = (request) -> Clipboard.paste(); null +pasteFromClipboard = (request) -> Clipboard.paste()  #  # Selects the tab with the ID specified in request.id @@ -256,7 +254,7 @@ BackgroundCommands =        if url == "pages/blank.html"          # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.          url = if tab.incognito then "chrome://newtab" else chrome.runtime.getURL url -      openUrlInNewTab { url }, callback +      TabOperations.openUrlInNewTab { url }, callback    duplicateTab: (callback) ->      chrome.tabs.getSelected(null, (tab) ->        chrome.tabs.duplicate(tab.id) @@ -296,8 +294,8 @@ BackgroundCommands =                scrollX: tabQueueEntry.scrollX,                scrollY: tabQueueEntry.scrollY)            callback())) -  openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() }) -  openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() }) +  openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab({ url: Clipboard.paste() }) +  openCopiedUrlInNewTab: (request) -> TabOperations.openUrlInNewTab({ url: Clipboard.paste() })    togglePinTab: (request) ->      chrome.tabs.getSelected(null, (tab) ->        chrome.tabs.update(tab.id, { pinned: !tab.pinned })) @@ -652,9 +650,9 @@ portHandlers =  sendRequestHandlers =    getCompletionKeys: getCompletionKeysRequest    getCurrentTabUrl: getCurrentTabUrl -  openUrlInNewTab: openUrlInNewTab -  openUrlInIncognito: openUrlInIncognito -  openUrlInCurrentTab: openUrlInCurrentTab +  openUrlInNewTab: TabOperations.openUrlInNewTab +  openUrlInIncognito: TabOperations.openUrlInIncognito +  openUrlInCurrentTab: TabOperations.openUrlInCurrentTab    openOptionsPageInNewTab: openOptionsPageInNewTab    registerFrame: registerFrame    unregisterFrame: unregisterFrame @@ -725,7 +723,7 @@ showUpgradeMessage = ->            Settings.set "previousVersion", currentVersion            chrome.notifications.onClicked.addListener (id) ->              if id == notificationId -              openUrlInNewTab url: "https://github.com/philc/vimium#release-notes" +              TabOperations.openUrlInNewTab url: "https://github.com/philc/vimium#release-notes"      else        # We need to wait for the user to accept the "notifications" permission.        chrome.permissions.onAdded.addListener showUpgradeMessage @@ -740,3 +738,5 @@ chrome.windows.getAll { populate: true }, (windows) ->        chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())  showUpgradeMessage() + +root.TabOperations = TabOperations diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee index 6e5f08ba..70ec1c17 100644 --- a/background_scripts/marks.coffee +++ b/background_scripts/marks.coffee @@ -82,7 +82,7 @@ Marks =            @gotoPositionInTab extend markInfo, tabId: tab.id        else          # There is no existing matching tab, we'll have to create one. -        chrome.tabs.create { url: @getBaseUrl markInfo.url }, (tab) => +        TabOperations.openUrlInNewTab { url: @getBaseUrl markInfo.url }, (tab) =>            # Note. tabLoadedHandlers is defined in "main.coffee".  The handler below will be called when the tab            # is loaded, its DOM is ready and it registers with the background page.            tabLoadedHandlers[tab.id] = => @gotoPositionInTab extend markInfo, tabId: tab.id 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..9b47cfbd 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..ce3caafe 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. @@ -375,8 +374,10 @@ class Movement extends CountPrefix    # it.    yank: (args = {}) ->      @yankedText = @selection.toString() -    @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument -    @selection.collapseToStart() unless @options.parentMode +    if @options.deleteFromDocument or args.deleteFromDocument +      @selection.deleteFromDocument() +    else +      @selection.collapseToStart()      message = @yankedText.replace /\s+/g, " "      message = message[...12] + "..." if 15 < @yankedText.length 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 bffbd457..9d850419 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 @@ -433,7 +429,7 @@ extend window,                  hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'                  # Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).                  @deactivateSingleton visibleInputs[selectedInputIndex].element -                visibleInputs[selectedInputIndex].element.focus() +                DomUtils.simulateSelect visibleInputs[selectedInputIndex].element                  @suppressEvent                else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey                  @exit() @@ -446,7 +442,7 @@ extend window,            # Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).            @deactivateSingleton visibleInputs[selectedInputIndex].element -          visibleInputs[selectedInputIndex].element.focus() +          DomUtils.simulateSelect visibleInputs[selectedInputIndex].element            if visibleInputs.length == 1              @exit()              return @@ -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/lib/settings.coffee b/lib/settings.coffee index 842f7618..99a20963 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -1,5 +1,17 @@ +# A "setting" is a stored key/value pair.  An "option" is a setting which has a default value and whose value +# can be changed on the options page. +# +# Option values which have never been changed by the user are in Settings.defaults. +# +# Settings whose values have been changed are: +# 1. stored either in chrome.storage.sync or in chrome.storage.local (but never both), and +# 2. cached in Settings.cache; on extension pages, Settings.cache uses localStorage (so it persists). +# +# In all cases except Settings.defaults, values are stored as jsonified strings. +  Settings = +  debug: false    storage: chrome.storage.sync    cache: {}    isLoaded: false @@ -11,18 +23,21 @@ Settings =        @cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage        @onLoaded() -    @storage.get null, (items) => -      unless chrome.runtime.lastError -        @handleUpdateFromChromeStorage key, value for own key, value of items +    chrome.storage.local.get null, (localItems) => +      localItems = {} if chrome.runtime.lastError +      @storage.get null, (syncedItems) => +        unless chrome.runtime.lastError +          @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems -      chrome.storage.onChanged.addListener (changes, area) => -        @propagateChangesFromChromeStorage changes if area == "sync" +        chrome.storage.onChanged.addListener (changes, area) => +          @propagateChangesFromChromeStorage changes if area == "sync" -      @onLoaded() +        @onLoaded()    # Called after @cache has been initialized.  On extension pages, this will be called twice, but that does    # not matter because it's idempotent.    onLoaded: -> +    @log "onLoaded: #{@onLoadedCallbacks.length} callback(s)"      @isLoaded = true      callback() while callback = @onLoadedCallbacks.pop() @@ -33,46 +48,40 @@ Settings =      @handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes    handleUpdateFromChromeStorage: (key, value) -> +    @log "handleUpdateFromChromeStorage: #{key}"      # Note: value here is either null or a JSONified string.  Therefore, even falsy settings values (like      # false, 0 or "") are truthy here.  Only null is falsy.      if @shouldSyncKey key        unless value and key of @cache and @cache[key] == value -        defaultValue = @defaults[key] -        defaultValueJSON = JSON.stringify defaultValue - -        if value and value != defaultValueJSON -          # Key/value has been changed to a non-default value. -          @cache[key] = value -          @performPostUpdateHook key, JSON.parse value -        else -          # The key has been reset to its default value. -          delete @cache[key] if key of @cache -          @performPostUpdateHook key, defaultValue +        value ?= JSON.stringify @defaults[key] +        @set key, JSON.parse(value), false    get: (key) ->      console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded      if key of @cache and @cache[key]? then JSON.parse @cache[key] else @defaults[key] -  set: (key, value) -> -    # Don't store the value if it is equal to the default, so we can change the defaults in the future. -    if JSON.stringify(value) == JSON.stringify @defaults[key] -      @clear key -    else -      jsonValue = JSON.stringify value -      @cache[key] = jsonValue -      if @shouldSyncKey key -        setting = {}; setting[key] = jsonValue +  set: (key, value, shouldSetInSyncedStorage = true) -> +    @cache[key] = JSON.stringify value +    @log "set: #{key} (length=#{@cache[key].length}, shouldSetInSyncedStorage=#{shouldSetInSyncedStorage})" +    if @shouldSyncKey key +      if shouldSetInSyncedStorage +        setting = {}; setting[key] = @cache[key] +        @log "   chrome.storage.sync.set(#{key})"          @storage.set setting -      @performPostUpdateHook key, value +      if Utils.isBackgroundPage() +        # Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below. +        @log "   chrome.storage.local.remove(#{key})" +        chrome.storage.local.remove key +    @performPostUpdateHook key, value    clear: (key) -> -    delete @cache[key] if @has key -    @storage.remove key if @shouldSyncKey key -    @performPostUpdateHook key, @get key +    @log "clear: #{key}" +    @set key, @defaults[key]    has: (key) -> key of @cache    use: (key, callback) -> +    @log "use: #{key} (isLoaded=#{@isLoaded})"      invokeCallback = => callback @get key      if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback @@ -80,6 +89,10 @@ Settings =    postUpdateHooks: {}    performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value +  # For development only. +  log: (args...) -> +    console.log "settings:", args... if @debug +    # Default values for all settings.    defaults:      scrollStepSize: 60 @@ -150,6 +163,7 @@ Settings =      settingsVersion: Utils.getCurrentVersion()      helpDialog_showAdvancedCommands: false +    optionsPage_showAdvancedOptions: false  Settings.init() @@ -169,5 +183,19 @@ if Utils.isBackgroundPage()        rawQuery = Settings.get "findModeRawQuery"        chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else []) +  # Migration (after 1.51, 2015/6/17). +  # Copy options with non-default values (and which are not in synced storage) to chrome.storage.local; +  # thereby making these settings accessible within content scripts. +  do (migrationKey = "copyNonDefaultsToChromeStorage-20150717") -> +    unless localStorage[migrationKey] +      chrome.storage.sync.get null, (items) -> +        unless chrome.runtime.lastError +          updates = {} +          for own key of localStorage +            if Settings.shouldSyncKey(key) and not items[key] +              updates[key] = localStorage[key] +          chrome.storage.local.set updates, -> +            localStorage[migrationKey] = not chrome.runtime.lastError +  root = exports ? window  root.Settings = Settings diff --git a/lib/utils.coffee b/lib/utils.coffee index 93045f32..d4beff03 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -323,10 +323,11 @@ class SimpleCache        null    rotate: (force = false) -> -    if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation -      @lastRotation = new Date() -      @previous = @cache -      @cache = {} +    Utils.nextTick => +      if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation +        @lastRotation = new Date() +        @previous = @cache +        @cache = {}    clear: ->      @rotate true 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/pages/options.coffee b/pages/options.coffee index 21e81c8f..1cbe88fa 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -181,6 +181,24 @@ class ExclusionRulesOnPopupOption extends ExclusionRulesOption      else        @url + "*" +Options = +  exclusionRules: ExclusionRulesOption +  filterLinkHints: CheckBoxOption +  hideHud: CheckBoxOption +  keyMappings: TextOption +  linkHintCharacters: NonEmptyTextOption +  linkHintNumbers: NonEmptyTextOption +  newTabUrl: NonEmptyTextOption +  nextPatterns: NonEmptyTextOption +  previousPatterns: NonEmptyTextOption +  regexFindMode: CheckBoxOption +  scrollStepSize: NumberOption +  smoothScroll: CheckBoxOption +  grabBackFocus: CheckBoxOption +  searchEngines: TextOption +  searchUrl: NonEmptyTextOption +  userDefinedLinkHintCss: TextOption +  initOptionsPage = ->    onUpdated = ->      $("saveOptions").removeAttribute "disabled" @@ -197,18 +215,20 @@ initOptionsPage = ->        show $("linkHintCharacters")        hide $("linkHintNumbers") -  toggleAdvancedOptions = -    do (advancedMode=false) -> -      (event) -> -        if advancedMode -          $("advancedOptions").style.display = "none" -          $("advancedOptionsButton").innerHTML = "Show Advanced Options" -        else -          $("advancedOptions").style.display = "table-row-group" -          $("advancedOptionsButton").innerHTML = "Hide Advanced Options" -        advancedMode = !advancedMode -        $("advancedOptionsButton").blur() -        event.preventDefault() +  maintainAdvancedOptions = -> +    if bgSettings.get "optionsPage_showAdvancedOptions" +      $("advancedOptions").style.display = "table-row-group" +      $("advancedOptionsButton").innerHTML = "Hide Advanced Options" +    else +      $("advancedOptions").style.display = "none" +      $("advancedOptionsButton").innerHTML = "Show Advanced Options" +  maintainAdvancedOptions() + +  toggleAdvancedOptions = (event) -> +    bgSettings.set "optionsPage_showAdvancedOptions", not bgSettings.get "optionsPage_showAdvancedOptions" +    maintainAdvancedOptions() +    $("advancedOptionsButton").blur() +    event.preventDefault()    activateHelpDialog = ->      showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId @@ -236,26 +256,8 @@ initOptionsPage = ->        document.activeElement.blur() if document?.activeElement?.blur        saveOptions() -  options = -    exclusionRules: ExclusionRulesOption -    filterLinkHints: CheckBoxOption -    hideHud: CheckBoxOption -    keyMappings: TextOption -    linkHintCharacters: NonEmptyTextOption -    linkHintNumbers: NonEmptyTextOption -    newTabUrl: NonEmptyTextOption -    nextPatterns: NonEmptyTextOption -    previousPatterns: NonEmptyTextOption -    regexFindMode: CheckBoxOption -    scrollStepSize: NumberOption -    smoothScroll: CheckBoxOption -    grabBackFocus: CheckBoxOption -    searchEngines: TextOption -    searchUrl: NonEmptyTextOption -    userDefinedLinkHintCss: TextOption -    # Populate options. The constructor adds each new object to "Option.all". -  for name, type of options +  for name, type of Options      new type(name,onUpdated)    maintainLinkHintsView() @@ -317,3 +319,6 @@ document.addEventListener "DOMContentLoaded", ->    xhr.send() +# Exported for tests. +root = exports ? window +root.Options = Options diff --git a/pages/options.html b/pages/options.html index 12a3ad21..22b041b7 100644 --- a/pages/options.html +++ b/pages/options.html @@ -283,7 +283,7 @@ b: http://b.com/?q=%s description                </span>              </td>              <td id="saveOptionsTableData" nowrap> -              <button id="advancedOptionsButton">Show Advanced Options</button> +              <button id="advancedOptionsButton"></button>                <button id="saveOptions" disabled="true">No Changes</button>              </td>            </tr> 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> diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee index 4a0cf746..9ce0a466 100644 --- a/tests/unit_tests/completion_test.coffee +++ b/tests/unit_tests/completion_test.coffee @@ -4,9 +4,6 @@ extend(global, require "../../background_scripts/completion_engines.js")  extend(global, require "../../background_scripts/completion.js")  extend global, require "./test_chrome_stubs.js" -global.document = -  createElement: -> {} -  context "bookmark completer",    setup ->      @bookmark3 = { title: "bookmark3", url: "bookmark3.com" } diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee index 08145190..9dc25cef 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -7,6 +7,7 @@ Utils.isBackgroundPage = -> true  Utils.isExtensionPage = -> true  global.localStorage = {}  extend(global,require "../../lib/settings.js") +extend(global,require "../../pages/options.js")  context "settings", @@ -27,12 +28,6 @@ context "settings",      Settings.set 'scrollStepSize', 20      assert.equal Settings.get('scrollStepSize'), 20 -  should "not store values equal to the default", -> -    Settings.set 'scrollStepSize', 20 -    assert.isTrue Settings.has 'scrollStepSize' -    Settings.set 'scrollStepSize', 60 -    assert.isFalse Settings.has 'scrollStepSize' -    should "revert to defaults if no key is stored", ->      Settings.set 'scrollStepSize', 20      Settings.clear 'scrollStepSize' @@ -55,7 +50,7 @@ context "synced settings",      Settings.set 'scrollStepSize', 20      assert.equal Settings.get('scrollStepSize'), 20      Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "60" } } -    assert.isFalse Settings.has 'scrollStepSize' +    assert.equal Settings.get('scrollStepSize'), 60    should "propagate non-default values from synced storage", ->      chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) } @@ -64,12 +59,12 @@ context "synced settings",    should "propagate default values from synced storage", ->      Settings.set 'scrollStepSize', 20      chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) } -    assert.isFalse Settings.has 'scrollStepSize' +    assert.equal Settings.get('scrollStepSize'), 60    should "clear a setting from synced storage", ->      Settings.set 'scrollStepSize', 20      chrome.storage.sync.remove 'scrollStepSize' -    assert.isFalse Settings.has 'scrollStepSize' +    assert.equal Settings.get('scrollStepSize'), 60    should "trigger a postUpdateHook", ->      message = "Hello World" @@ -80,3 +75,10 @@ context "synced settings",    should "sync a key which is not a known setting (without crashing)", ->      chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") } + +context "default valuess", + +  should "have a default value for every option", -> +    for own key of Options +      assert.isTrue key of Settings.defaults + diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index fe2fc298..0bb6ed81 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -8,6 +8,10 @@  exports.window = {}  exports.localStorage = {} +global.document = +  createElement: -> {} +  addEventListener: -> +  exports.chrome =    runtime:      getManifest: () -> @@ -21,6 +25,7 @@ exports.chrome =    extension:      getURL: (path) -> path +    getBackgroundPage: -> {}    tabs:      onSelectionChanged: @@ -57,9 +62,9 @@ exports.chrome =    storage:      # chrome.storage.local      local: -      get: -> -      set: -> -      remove: -> +      get: (_, callback) -> callback?() +      set: (_, callback) -> callback?() +      remove: (_, callback) -> callback?()      # chrome.storage.onChanged      onChanged: | 
