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 | ... | ... |