Merge branch 'acet-repo-editor-fix-link-linking' into 'master'
RepoEditor: Implement line and range linking. Closes #38254 See merge request gitlab-org/gitlab-ce!14448
This commit is contained in:
commit
e9f7d26f07
4 changed files with 159 additions and 144 deletions
|
@ -28,148 +28,149 @@
|
|||
// </div>
|
||||
// </div>
|
||||
//
|
||||
(function() {
|
||||
this.LineHighlighter = (function() {
|
||||
// CSS class applied to highlighted lines
|
||||
LineHighlighter.prototype.highlightClass = 'hll';
|
||||
|
||||
// Internal copy of location.hash so we're not dependent on `location` in tests
|
||||
LineHighlighter.prototype._hash = '';
|
||||
const LineHighlighter = function(options = {}) {
|
||||
options.highlightLineClass = options.highlightLineClass || 'hll';
|
||||
options.fileHolderSelector = options.fileHolderSelector || '.file-holder';
|
||||
options.scrollFileHolder = options.scrollFileHolder || false;
|
||||
options.hash = options.hash || location.hash;
|
||||
|
||||
function LineHighlighter(hash) {
|
||||
if (hash == null) {
|
||||
// Initialize a LineHighlighter object
|
||||
//
|
||||
// hash - String URL hash for dependency injection in tests
|
||||
hash = location.hash;
|
||||
this.options = options;
|
||||
this._hash = options.hash;
|
||||
this.highlightLineClass = options.highlightLineClass;
|
||||
this.setHash = this.setHash.bind(this);
|
||||
this.highlightLine = this.highlightLine.bind(this);
|
||||
this.clickHandler = this.clickHandler.bind(this);
|
||||
this.highlightHash = this.highlightHash.bind(this);
|
||||
|
||||
this.bindEvents();
|
||||
this.highlightHash();
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
const $fileHolder = $(this.options.fileHolderSelector);
|
||||
|
||||
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
|
||||
$fileHolder.on('highlight:line', this.highlightHash);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.highlightHash = function() {
|
||||
var range;
|
||||
|
||||
if (this._hash !== '') {
|
||||
range = this.hashToRange(this._hash);
|
||||
|
||||
if (range[0]) {
|
||||
this.highlightRange(range);
|
||||
const lineSelector = `#L${range[0]}`;
|
||||
const scrollOptions = {
|
||||
// Scroll to the first highlighted line on initial load
|
||||
// Offset -50 for the sticky top bar, and another -100 for some context
|
||||
offset: -150
|
||||
};
|
||||
if (this.options.scrollFileHolder) {
|
||||
$(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions);
|
||||
} else {
|
||||
$.scrollTo(lineSelector, scrollOptions);
|
||||
}
|
||||
this.setHash = this.setHash.bind(this);
|
||||
this.highlightLine = this.highlightLine.bind(this);
|
||||
this.clickHandler = this.clickHandler.bind(this);
|
||||
this.highlightHash = this.highlightHash.bind(this);
|
||||
this._hash = hash;
|
||||
this.bindEvents();
|
||||
this.highlightHash();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
const $fileHolder = $('.file-holder');
|
||||
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
|
||||
$fileHolder.on('highlight:line', this.highlightHash);
|
||||
};
|
||||
LineHighlighter.prototype.clickHandler = function(event) {
|
||||
var current, lineNumber, range;
|
||||
event.preventDefault();
|
||||
this.clearHighlight();
|
||||
lineNumber = $(event.target).closest('a').data('line-number');
|
||||
current = this.hashToRange(this._hash);
|
||||
if (!(current[0] && event.shiftKey)) {
|
||||
// If there's no current selection, or there is but Shift wasn't held,
|
||||
// treat this like a single-line selection.
|
||||
this.setHash(lineNumber);
|
||||
return this.highlightLine(lineNumber);
|
||||
} else if (event.shiftKey) {
|
||||
if (lineNumber < current[0]) {
|
||||
range = [lineNumber, current[0]];
|
||||
} else {
|
||||
range = [current[0], lineNumber];
|
||||
}
|
||||
this.setHash(range[0], range[1]);
|
||||
return this.highlightRange(range);
|
||||
}
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.highlightHash = function() {
|
||||
var range;
|
||||
if (this._hash !== '') {
|
||||
range = this.hashToRange(this._hash);
|
||||
if (range[0]) {
|
||||
this.highlightRange(range);
|
||||
$.scrollTo("#L" + range[0], {
|
||||
// Scroll to the first highlighted line on initial load
|
||||
// Offset -50 for the sticky top bar, and another -100 for some context
|
||||
offset: -150
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
LineHighlighter.prototype.clearHighlight = function() {
|
||||
return $("." + this.highlightLineClass).removeClass(this.highlightLineClass);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.clickHandler = function(event) {
|
||||
var current, lineNumber, range;
|
||||
event.preventDefault();
|
||||
this.clearHighlight();
|
||||
lineNumber = $(event.target).closest('a').data('line-number');
|
||||
current = this.hashToRange(this._hash);
|
||||
if (!(current[0] && event.shiftKey)) {
|
||||
// If there's no current selection, or there is but Shift wasn't held,
|
||||
// treat this like a single-line selection.
|
||||
this.setHash(lineNumber);
|
||||
return this.highlightLine(lineNumber);
|
||||
} else if (event.shiftKey) {
|
||||
if (lineNumber < current[0]) {
|
||||
range = [lineNumber, current[0]];
|
||||
} else {
|
||||
range = [current[0], lineNumber];
|
||||
}
|
||||
this.setHash(range[0], range[1]);
|
||||
return this.highlightRange(range);
|
||||
}
|
||||
};
|
||||
// Convert a URL hash String into line numbers
|
||||
//
|
||||
// hash - Hash String
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// hashToRange('#L5') # => [5, null]
|
||||
// hashToRange('#L5-15') # => [5, 15]
|
||||
// hashToRange('#foo') # => [null, null]
|
||||
//
|
||||
// Returns an Array
|
||||
LineHighlighter.prototype.hashToRange = function(hash) {
|
||||
var first, last, matches;
|
||||
// ?L(\d+)(?:-(\d+))?$/)
|
||||
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
|
||||
if (matches && matches.length) {
|
||||
first = parseInt(matches[1], 10);
|
||||
last = matches[2] ? parseInt(matches[2], 10) : null;
|
||||
return [first, last];
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.clearHighlight = function() {
|
||||
return $("." + this.highlightClass).removeClass(this.highlightClass);
|
||||
// Unhighlight previously highlighted lines
|
||||
};
|
||||
// Highlight a single line
|
||||
//
|
||||
// lineNumber - Line number to highlight
|
||||
LineHighlighter.prototype.highlightLine = function(lineNumber) {
|
||||
return $("#LC" + lineNumber).addClass(this.highlightLineClass);
|
||||
};
|
||||
|
||||
// Convert a URL hash String into line numbers
|
||||
//
|
||||
// hash - Hash String
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// hashToRange('#L5') # => [5, null]
|
||||
// hashToRange('#L5-15') # => [5, 15]
|
||||
// hashToRange('#foo') # => [null, null]
|
||||
//
|
||||
// Returns an Array
|
||||
LineHighlighter.prototype.hashToRange = function(hash) {
|
||||
var first, last, matches;
|
||||
// ?L(\d+)(?:-(\d+))?$/)
|
||||
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
|
||||
if (matches && matches.length) {
|
||||
first = parseInt(matches[1], 10);
|
||||
last = matches[2] ? parseInt(matches[2], 10) : null;
|
||||
return [first, last];
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
};
|
||||
// Highlight all lines within a range
|
||||
//
|
||||
// range - Array containing the starting and ending line numbers
|
||||
LineHighlighter.prototype.highlightRange = function(range) {
|
||||
var i, lineNumber, ref, ref1, results;
|
||||
if (range[1]) {
|
||||
results = [];
|
||||
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
|
||||
results.push(this.highlightLine(lineNumber));
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return this.highlightLine(range[0]);
|
||||
}
|
||||
};
|
||||
|
||||
// Highlight a single line
|
||||
//
|
||||
// lineNumber - Line number to highlight
|
||||
LineHighlighter.prototype.highlightLine = function(lineNumber) {
|
||||
return $("#LC" + lineNumber).addClass(this.highlightClass);
|
||||
};
|
||||
// Set the URL hash string
|
||||
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
|
||||
var hash;
|
||||
if (lastLineNumber) {
|
||||
hash = "#L" + firstLineNumber + "-" + lastLineNumber;
|
||||
} else {
|
||||
hash = "#L" + firstLineNumber;
|
||||
}
|
||||
this._hash = hash;
|
||||
return this.__setLocationHash__(hash);
|
||||
};
|
||||
|
||||
// Highlight all lines within a range
|
||||
//
|
||||
// range - Array containing the starting and ending line numbers
|
||||
LineHighlighter.prototype.highlightRange = function(range) {
|
||||
var i, lineNumber, ref, ref1, results;
|
||||
if (range[1]) {
|
||||
results = [];
|
||||
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
|
||||
results.push(this.highlightLine(lineNumber));
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return this.highlightLine(range[0]);
|
||||
}
|
||||
};
|
||||
// Make the actual hash change in the browser
|
||||
//
|
||||
// This method is stubbed in tests.
|
||||
LineHighlighter.prototype.__setLocationHash__ = function(value) {
|
||||
return history.pushState({
|
||||
url: value
|
||||
// We're using pushState instead of assigning location.hash directly to
|
||||
// prevent the page from scrolling on the hashchange event
|
||||
}, document.title, value);
|
||||
};
|
||||
|
||||
// Set the URL hash string
|
||||
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
|
||||
var hash;
|
||||
if (lastLineNumber) {
|
||||
hash = "#L" + firstLineNumber + "-" + lastLineNumber;
|
||||
} else {
|
||||
hash = "#L" + firstLineNumber;
|
||||
}
|
||||
this._hash = hash;
|
||||
return this.__setLocationHash__(hash);
|
||||
};
|
||||
|
||||
// Make the actual hash change in the browser
|
||||
//
|
||||
// This method is stubbed in tests.
|
||||
LineHighlighter.prototype.__setLocationHash__ = function(value) {
|
||||
return history.pushState({
|
||||
url: value
|
||||
// We're using pushState instead of assigning location.hash directly to
|
||||
// prevent the page from scrolling on the hashchange event
|
||||
}, document.title, value);
|
||||
};
|
||||
|
||||
return LineHighlighter;
|
||||
})();
|
||||
}).call(window);
|
||||
window.LineHighlighter = LineHighlighter;
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
<script>
|
||||
/* global LineHighlighter */
|
||||
|
||||
import Store from '../stores/repo_store';
|
||||
|
||||
export default {
|
||||
data: () => Store,
|
||||
mounted() {
|
||||
this.highlightFile();
|
||||
},
|
||||
computed: {
|
||||
html() {
|
||||
return this.activeFile.html;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
highlightFile() {
|
||||
$(this.$el).find('.file-content').syntaxHighlight();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.highlightFile();
|
||||
this.lineHighlighter = new LineHighlighter({
|
||||
fileHolderSelector: '.blob-viewer-container',
|
||||
scrollFileHolder: true,
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
html() {
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
border-radius: $border-radius-default;
|
||||
color: $almost-black;
|
||||
|
||||
.code.white pre .hll {
|
||||
background-color: $well-light-border !important;
|
||||
}
|
||||
|
||||
.tree-content-holder {
|
||||
display: flex;
|
||||
min-height: 300px;
|
||||
|
|
|
@ -18,19 +18,25 @@ import '~/line_highlighter';
|
|||
beforeEach(function() {
|
||||
loadFixtures('static/line_highlighter.html.raw');
|
||||
this["class"] = new LineHighlighter();
|
||||
this.css = this["class"].highlightClass;
|
||||
this.css = this["class"].highlightLineClass;
|
||||
return this.spies = {
|
||||
__setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {})
|
||||
};
|
||||
});
|
||||
describe('behavior', function() {
|
||||
it('highlights one line given in the URL hash', function() {
|
||||
new LineHighlighter('#L13');
|
||||
new LineHighlighter({ hash: '#L13' });
|
||||
return expect($('#LC13')).toHaveClass(this.css);
|
||||
});
|
||||
it('highlights one line given in the URL hash with given CSS class name', function() {
|
||||
const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' });
|
||||
expect(hiliter.highlightLineClass).toBe('hilite');
|
||||
expect($('#LC13')).toHaveClass('hilite');
|
||||
expect($('#LC13')).not.toHaveClass('hll');
|
||||
});
|
||||
it('highlights a range of lines given in the URL hash', function() {
|
||||
var line, results;
|
||||
new LineHighlighter('#L5-25');
|
||||
new LineHighlighter({ hash: '#L5-25' });
|
||||
expect($("." + this.css).length).toBe(21);
|
||||
results = [];
|
||||
for (line = 5; line <= 25; line += 1) {
|
||||
|
@ -41,7 +47,7 @@ import '~/line_highlighter';
|
|||
it('scrolls to the first highlighted line on initial load', function() {
|
||||
var spy;
|
||||
spy = spyOn($, 'scrollTo');
|
||||
new LineHighlighter('#L5-25');
|
||||
new LineHighlighter({ hash: '#L5-25' });
|
||||
return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything());
|
||||
});
|
||||
it('discards click events', function() {
|
||||
|
@ -50,10 +56,10 @@ import '~/line_highlighter';
|
|||
clickLine(13);
|
||||
return expect(spy).toHaveBeenPrevented();
|
||||
});
|
||||
return it('handles garbage input from the hash', function() {
|
||||
it('handles garbage input from the hash', function() {
|
||||
var func;
|
||||
func = function() {
|
||||
return new LineHighlighter('#blob-content-holder');
|
||||
return new LineHighlighter({ fileHolderSelector: '#blob-content-holder' });
|
||||
};
|
||||
return expect(func).not.toThrow();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue