Commit 568e05a73b1c4aac814f75da488d3d1ed7d847e9

Authored by Jason Hollingsworth
1 parent 09d00563

Enable multiline select for blobs.

Holding shift will allow users to click and select multiple lines.
The behavior is similar to GitHub.
Complete with tests.
app/assets/javascripts/blob.js.coffee
1 1 class BlobView
2 2 constructor: ->
  3 + # handle multi-line select
  4 + handleMultiSelect = (e) ->
  5 + [ first_line, last_line ] = parseSelectedLines()
  6 + [ line_number ] = parseSelectedLines($(this).attr("id"))
  7 + hash = "L#{line_number}"
  8 +
  9 + if e.shiftKey and not isNaN(first_line) and not isNaN(line_number)
  10 + if line_number < first_line
  11 + last_line = first_line
  12 + first_line = line_number
  13 + else
  14 + last_line = line_number
  15 +
  16 + hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}"
  17 +
  18 + setHash(hash)
  19 + e.preventDefault()
  20 +
3 21 # See if there are lines selected
4 22 # "#L12" and "#L34-56" supported
5   - highlightBlobLines = ->
6   - if window.location.hash isnt ""
7   - matches = window.location.hash.match(/\#L(\d+)(\-(\d+))?/)
  23 + highlightBlobLines = (e) ->
  24 + [ first_line, last_line ] = parseSelectedLines()
  25 +
  26 + unless isNaN first_line
  27 + $("#tree-content-holder .highlight .line").removeClass("hll")
  28 + $("#LC#{line}").addClass("hll") for line in [first_line..last_line]
  29 + $("#L#{first_line}").ScrollTo() unless e?
  30 +
  31 + # parse selected lines from hash
  32 + # always return first and last line (initialized to NaN)
  33 + parseSelectedLines = (str) ->
  34 + first_line = NaN
  35 + last_line = NaN
  36 + hash = str || window.location.hash
  37 +
  38 + if hash isnt ""
  39 + matches = hash.match(/\#?L(\d+)(\-(\d+))?/)
8 40 first_line = parseInt(matches?[1])
9 41 last_line = parseInt(matches?[3])
  42 + last_line = first_line if isNaN(last_line)
  43 +
  44 + [ first_line, last_line ]
  45 +
  46 + setHash = (hash) ->
  47 + hash = hash.replace(/^\#/, "")
  48 + nodes = $("#" + hash)
  49 + # if any nodes are using this id, they must be temporarily changed
  50 + # also, add a temporary div at the top of the screen to prevent scrolling
  51 + if nodes.length > 0
  52 + scroll_top = $(document).scrollTop()
  53 + nodes.attr("id", "")
  54 + tmp = $("<div></div>")
  55 + .css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" })
  56 + .attr("id", hash)
  57 + .appendTo(document.body)
  58 +
  59 + window.location.hash = hash
  60 +
  61 + # restore the nodes
  62 + if nodes.length > 0
  63 + tmp.remove()
  64 + nodes.attr("id", hash)
10 65  
11   - unless isNaN first_line
12   - last_line = first_line if isNaN(last_line)
13   - $("#tree-content-holder .highlight .line").removeClass("hll")
14   - $("#LC#{line}").addClass("hll") for line in [first_line..last_line]
15   - $("#L#{first_line}").ScrollTo()
  66 + # initialize multi-line select
  67 + $("#tree-content-holder .line_numbers a[id^=L]").on("click", handleMultiSelect)
16 68  
17 69 # Highlight the correct lines on load
18 70 highlightBlobLines()
19 71  
20 72 # Highlight the correct lines when the hash part of the URL changes
21   - $(window).on 'hashchange', highlightBlobLines
  73 + $(window).on("hashchange", highlightBlobLines)
22 74  
23 75  
24 76 @BlobView = BlobView
... ...
features/project/source/multiselect_blob.feature 0 → 100644
... ... @@ -0,0 +1,86 @@
  1 +Feature: Project Multiselect Blob
  2 + Background:
  3 + Given I sign in as a user
  4 + And I own project "Shop"
  5 + And I visit project source page
  6 + And I click on "Gemfile.lock" file in repo
  7 +
  8 + @javascript
  9 + Scenario: I click line 1 in file
  10 + When I click line 1 in file
  11 + Then I should see "L1" as URI fragment
  12 + And I should see line 1 highlighted
  13 +
  14 + @javascript
  15 + Scenario: I shift-click line 1 in file
  16 + When I shift-click line 1 in file
  17 + Then I should see "L1" as URI fragment
  18 + And I should see line 1 highlighted
  19 +
  20 + @javascript
  21 + Scenario: I click line 1 then click line 2 in file
  22 + When I click line 1 in file
  23 + Then I should see "L1" as URI fragment
  24 + And I should see line 1 highlighted
  25 + Then I click line 2 in file
  26 + Then I should see "L2" as URI fragment
  27 + And I should see line 2 highlighted
  28 +
  29 + @javascript
  30 + Scenario: I click various line numbers to test multiselect
  31 + Then I click line 1 in file
  32 + Then I should see "L1" as URI fragment
  33 + And I should see line 1 highlighted
  34 + Then I shift-click line 2 in file
  35 + Then I should see "L1-2" as URI fragment
  36 + And I should see lines 1-2 highlighted
  37 + Then I shift-click line 3 in file
  38 + Then I should see "L1-3" as URI fragment
  39 + And I should see lines 1-3 highlighted
  40 + Then I click line 3 in file
  41 + Then I should see "L3" as URI fragment
  42 + And I should see line 3 highlighted
  43 + Then I shift-click line 1 in file
  44 + Then I should see "L1-3" as URI fragment
  45 + And I should see lines 1-3 highlighted
  46 + Then I shift-click line 5 in file
  47 + Then I should see "L1-5" as URI fragment
  48 + And I should see lines 1-5 highlighted
  49 + Then I shift-click line 4 in file
  50 + Then I should see "L1-4" as URI fragment
  51 + And I should see lines 1-4 highlighted
  52 + Then I click line 5 in file
  53 + Then I should see "L5" as URI fragment
  54 + And I should see line 5 highlighted
  55 + Then I shift-click line 3 in file
  56 + Then I should see "L3-5" as URI fragment
  57 + And I should see lines 3-5 highlighted
  58 + Then I shift-click line 1 in file
  59 + Then I should see "L1-3" as URI fragment
  60 + And I should see lines 1-3 highlighted
  61 + Then I shift-click line 1 in file
  62 + Then I should see "L1" as URI fragment
  63 + And I should see line 1 highlighted
  64 +
  65 + @javascript
  66 + Scenario: I multiselect lines 1-5 and then go back and forward in history
  67 + When I click line 1 in file
  68 + And I shift-click line 3 in file
  69 + And I shift-click line 2 in file
  70 + And I shift-click line 5 in file
  71 + Then I should see "L1-5" as URI fragment
  72 + And I should see lines 1-5 highlighted
  73 + Then I go back in history
  74 + Then I should see "L1-2" as URI fragment
  75 + And I should see lines 1-2 highlighted
  76 + Then I go back in history
  77 + Then I should see "L1-3" as URI fragment
  78 + And I should see lines 1-3 highlighted
  79 + Then I go back in history
  80 + Then I should see "L1" as URI fragment
  81 + And I should see line 1 highlighted
  82 + Then I go forward in history
  83 + And I go forward in history
  84 + And I go forward in history
  85 + Then I should see "L1-5" as URI fragment
  86 + And I should see lines 1-5 highlighted
0 87 \ No newline at end of file
... ...
features/steps/project/project_multiselect_blob.rb 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +class ProjectMultiselectBlob < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedProject
  4 + include SharedPaths
  5 +
  6 + class << self
  7 + def click_line_steps(*line_numbers)
  8 + line_numbers.each do |line_number|
  9 + step "I click line #{line_number} in file" do
  10 + find("#L#{line_number}").click
  11 + end
  12 +
  13 + step "I shift-click line #{line_number} in file" do
  14 + script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));"
  15 + page.evaluate_script(script)
  16 + end
  17 + end
  18 + end
  19 +
  20 + def check_state_steps(*ranges)
  21 + ranges.each do |range|
  22 + fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}"
  23 + pluralization = range.kind_of?(Array) ? "s" : ""
  24 +
  25 + step "I should see \"#{fragment}\" as URI fragment" do
  26 + URI.parse(current_url).fragment.should == fragment
  27 + end
  28 +
  29 + step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do
  30 + ids = Array(range).map { |n| "LC#{n}" }
  31 + extra = false
  32 +
  33 + highlighted = all("#tree-content-holder .highlight .line.hll")
  34 + highlighted.each do |element|
  35 + extra ||= ids.delete(element[:id]).nil?
  36 + end
  37 +
  38 + extra.should be_false and ids.should be_empty
  39 + end
  40 + end
  41 + end
  42 + end
  43 +
  44 + click_line_steps *Array(1..5)
  45 + check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5)
  46 +
  47 + step 'I go back in history' do
  48 + page.evaluate_script("window.history.back()")
  49 + end
  50 +
  51 + step 'I go forward in history' do
  52 + page.evaluate_script("window.history.forward()")
  53 + end
  54 +
  55 + step 'I click on "Gemfile.lock" file in repo' do
  56 + click_link "Gemfile.lock"
  57 + end
  58 +end
0 59 \ No newline at end of file
... ...