Commit 568e05a73b1c4aac814f75da488d3d1ed7d847e9
1 parent
09d00563
Exists in
master
and in
4 other branches
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.
Showing
3 changed files
with
205 additions
and
9 deletions
Show diff stats
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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |