line_highlighter.js.coffee 4.34 KB
Newer Older
1
# LineHighlighter
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# Handles single- and multi-line selection and highlight for blob views.
#
#= require jquery.scrollTo
#
# ### Example Markup
#
#   <div id="tree-content-holder">
#     <div class="file-content">
#       <div class="line-numbers">
#         <a href="#L1" id="L1" data-line-number="1">1</a>
#         <a href="#L2" id="L2" data-line-number="2">2</a>
#         <a href="#L3" id="L3" data-line-number="3">3</a>
#         <a href="#L4" id="L4" data-line-number="4">4</a>
#         <a href="#L5" id="L5" data-line-number="5">5</a>
#       </div>
#       <pre class="code highlight">
#         <code>
#           <span id="LC1" class="line">...</span>
#           <span id="LC2" class="line">...</span>
#           <span id="LC3" class="line">...</span>
#           <span id="LC4" class="line">...</span>
#           <span id="LC5" class="line">...</span>
#         </code>
#       </pre>
#     </div>
#   </div>
Robert Speicher's avatar
Robert Speicher committed
29
#
30
class @LineHighlighter
Robert Speicher's avatar
Robert Speicher committed
31 32 33
  # CSS class applied to highlighted lines
  highlightClass: 'hll'

34
  # Internal copy of location.hash so we're not dependent on `location` in tests
Robert Speicher's avatar
Robert Speicher committed
35
  _hash: ''
36

37
  # Initialize a LineHighlighter object
38 39 40 41 42 43 44 45 46 47
  #
  # hash - String URL hash for dependency injection in tests
  constructor: (hash = location.hash) ->
    @_hash = hash

    @bindEvents()

    unless hash == ''
      range = @hashToRange(hash)

Robert Speicher's avatar
Robert Speicher committed
48
      if range[0]
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
        @highlightRange(range)

        # Scroll to the first highlighted line on initial load
        # Offset -50 for the sticky top bar, and another -100 for some context
        $.scrollTo("#L#{range[0]}", offset: -150)

  bindEvents: ->
    $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler

    # While it may seem odd to bind to the mousedown event and then throw away
    # the click event, there is a method to our madness.
    #
    # If not done this way, the line number anchor will sometimes keep its
    # active state even when the event is cancelled, resulting in an ugly border
    # around the link and/or a persisted underline text decoration.

    $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) ->
      event.preventDefault()

  clickHandler: (event) =>
    event.preventDefault()

71 72
    @clearHighlight()

73
    lineNumber = $(event.target).closest('a').data('line-number')
74 75
    current = @hashToRange(@_hash)

76
    unless current[0] && event.shiftKey
77 78 79 80 81 82 83 84 85 86 87 88 89
      # If there's no current selection, or there is but Shift wasn't held,
      # treat this like a single-line selection.
      @setHash(lineNumber)
      @highlightLine(lineNumber)
    else if event.shiftKey
      if lineNumber < current[0]
        range = [lineNumber, current[0]]
      else
        range = [current[0], lineNumber]

      @setHash(range[0], range[1])
      @highlightRange(range)

90 91
  # Unhighlight previously highlighted lines
  clearHighlight: ->
Robert Speicher's avatar
Robert Speicher committed
92
    $(".#{@highlightClass}").removeClass(@highlightClass)
93

94 95 96 97 98 99
  # Convert a URL hash String into line numbers
  #
  # hash - Hash String
  #
  # Examples:
  #
Robert Speicher's avatar
Robert Speicher committed
100
  #   hashToRange('#L5')    # => [5, null]
101
  #   hashToRange('#L5-15') # => [5, 15]
Robert Speicher's avatar
Robert Speicher committed
102
  #   hashToRange('#foo')   # => [null, null]
103 104 105
  #
  # Returns an Array
  hashToRange: (hash) ->
Robert Speicher's avatar
Robert Speicher committed
106 107
    matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/)

108
    if matches && matches.length
Robert Speicher's avatar
Robert Speicher committed
109
      first = parseInt(matches[1])
110
      last  = if matches[2] then parseInt(matches[2]) else null
111

Robert Speicher's avatar
Robert Speicher committed
112 113 114
      [first, last]
    else
      [null, null]
115 116 117

  # Highlight a single line
  #
Robert Speicher's avatar
Robert Speicher committed
118 119 120
  # lineNumber - Line number to highlight
  highlightLine: (lineNumber) =>
    $("#LC#{lineNumber}").addClass(@highlightClass)
121 122 123

  # Highlight all lines within a range
  #
Robert Speicher's avatar
Robert Speicher committed
124
  # range - Array containing the starting and ending line numbers
125
  highlightRange: (range) ->
Robert Speicher's avatar
Robert Speicher committed
126
    if range[1]
127 128
      for lineNumber in [range[0]..range[1]]
        @highlightLine(lineNumber)
Robert Speicher's avatar
Robert Speicher committed
129 130
    else
      @highlightLine(range[0])
131

Robert Speicher's avatar
Robert Speicher committed
132
  # Set the URL hash string
133
  setHash: (firstLineNumber, lastLineNumber) =>
Robert Speicher's avatar
Robert Speicher committed
134
    if lastLineNumber
135
      hash = "#L#{firstLineNumber}-#{lastLineNumber}"
Robert Speicher's avatar
Robert Speicher committed
136 137
    else
      hash = "#L#{firstLineNumber}"
138 139 140 141

    @_hash = hash
    @__setLocationHash__(hash)

142
  # Make the actual hash change in the browser
143 144 145
  #
  # This method is stubbed in tests.
  __setLocationHash__: (value) ->
146 147 148
    # We're using pushState instead of assigning location.hash directly to
    # prevent the page from scrolling on the hashchange event
    history.pushState({turbolinks: false, url: value}, document.title, value)